mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-04 23:45:07 +00:00
feat(console): integrate frontend (#95)
* feat: console frontend * chore(dependabot): cycle and npm * chore: rename citadel to zitadel, remove generated files * chore: delete go files * chore(frontend): ci steps * chore: remove docker and envoy files * chore: remove docker file * chore: working dir * chore: run proto build * add console start * chore: restructure folders * chore: remove gui build * statikFs * generate proto for console * add statik import * import * chore: try statik * chore: path * chore: path * chore: script in root * chore: order build steps * chore: go get * chore: folder traversal * chore: non empty test file * chore: gitignore * chore: gitignore * chore: statik path * chore: switch to failing FE build * fix: build * fix: project-grant-test * fix: rm test * add statik.go * go mod tidy * chore: place test, seperate test from build * chore: lint all the world * chore: ci the world instead * chore: tune docker * chore: undo container test * chore: fix run * chore: docker build * chore: test docker build * chore: go build flags * finaly * fix caos_local * go mod Co-authored-by: Livio Amstutz <livio.a@gmail.com> Co-authored-by: Max Peintner <max@caos.ch>
This commit is contained in:
parent
9e32740eb8
commit
92a294f5c8
@ -2,7 +2,13 @@ version: 1
|
|||||||
update_configs:
|
update_configs:
|
||||||
- package_manager: "go:modules"
|
- package_manager: "go:modules"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
update_schedule: "daily"
|
update_schedule: "weekly"
|
||||||
|
commit_message:
|
||||||
|
prefix: "chore"
|
||||||
|
include_scope: true
|
||||||
|
- package_manager: "javascript"
|
||||||
|
directory: "/console"
|
||||||
|
update_schedule: "weekly"
|
||||||
commit_message:
|
commit_message:
|
||||||
prefix: "chore"
|
prefix: "chore"
|
||||||
include_scope: true
|
include_scope: true
|
101
.github/workflows/release.yml
vendored
101
.github/workflows/release.yml
vendored
@ -10,24 +10,51 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
angular: # TODO Implement proper build and cache and coverage upload
|
angular-test: # will be added later on
|
||||||
runs-on: ubuntu-18.04
|
runs-on: ubuntu-18.04
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./console
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-node@v1
|
- uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
- run: echo "hodor" > hodor.txt
|
- run: npm ci
|
||||||
# - run: npm ci
|
#- run: npm test
|
||||||
# - run: npm run lint
|
- run: echo "replace me with real test"
|
||||||
# - run: npm run prodbuild
|
|
||||||
# - run: npm test
|
angular-lint: # will be added later on
|
||||||
|
runs-on: ubuntu-18.04
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./console
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run lint
|
||||||
|
|
||||||
|
angular-build:
|
||||||
|
runs-on: ubuntu-18.04
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./console
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run prodbuild
|
||||||
- uses: actions/upload-artifact@v1
|
- uses: actions/upload-artifact@v1
|
||||||
with:
|
with:
|
||||||
name: angular
|
name: angular
|
||||||
path: hodor.txt
|
path: console/dist/console
|
||||||
|
|
||||||
go: # TODO Implement proper build and cache
|
go-test:
|
||||||
runs-on: ubuntu-18.04
|
runs-on: ubuntu-18.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
@ -35,37 +62,59 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
- run: go test -race -v -coverprofile=profile.cov ./...
|
- run: go test -race -v -coverprofile=profile.cov ./...
|
||||||
- run: go build -o zitadel cmd/zitadel/main.go
|
|
||||||
- uses: actions/upload-artifact@v1
|
- uses: actions/upload-artifact@v1
|
||||||
with:
|
with:
|
||||||
name: go-coverage
|
name: go-coverage
|
||||||
path: profile.cov
|
path: profile.cov
|
||||||
- uses: actions/upload-artifact@v1
|
|
||||||
with:
|
|
||||||
name: go-binary
|
|
||||||
path: zitadel
|
|
||||||
- uses: codecov/codecov-action@v1
|
- uses: codecov/codecov-action@v1
|
||||||
with:
|
with:
|
||||||
file: ./profile.cov
|
file: ./profile.cov
|
||||||
name: codecov-go
|
name: codecov-go
|
||||||
|
|
||||||
container-prod: # Artifact paths need better place
|
go-lint:
|
||||||
runs-on: ubuntu-18.04
|
runs-on: ubuntu-18.04
|
||||||
needs: [angular, go]
|
|
||||||
steps:
|
steps:
|
||||||
- name: Source checkout
|
- uses: actions/checkout@v2
|
||||||
uses: actions/checkout@v2
|
- uses: actions/setup-go@v2-beta
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- run: echo "replace me with real lint"
|
||||||
|
|
||||||
|
go-build:
|
||||||
|
runs-on: ubuntu-18.04
|
||||||
|
needs: [angular-build, angular-test, angular-lint, go-test] ### We need the artifact from the angular build and that's why we wait here
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-go@v2-beta
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
- uses: actions/download-artifact@v1
|
- uses: actions/download-artifact@v1
|
||||||
with:
|
with:
|
||||||
name: angular
|
name: angular
|
||||||
path: .build/angular
|
path: console/dist/app
|
||||||
|
- run: go get github.com/rakyll/statik
|
||||||
|
- run: ./build/console/generate-static.sh
|
||||||
|
- run: cat pkg/console/statik/statik.go
|
||||||
|
- run: CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags '-extldflags "-static"' -o zitadel cmd/zitadel/main.go
|
||||||
|
- uses: actions/upload-artifact@v1
|
||||||
|
with:
|
||||||
|
name: go-binary
|
||||||
|
path: zitadel
|
||||||
|
|
||||||
|
container-prod:
|
||||||
|
runs-on: ubuntu-18.04
|
||||||
|
needs: go-build
|
||||||
|
steps:
|
||||||
|
- name: Source checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
- uses: actions/download-artifact@v1
|
- uses: actions/download-artifact@v1
|
||||||
with:
|
with:
|
||||||
name: go-binary
|
name: go-binary
|
||||||
path: .build/go
|
path: .build/go
|
||||||
- uses: docker/build-push-action@v1
|
- uses: docker/build-push-action@v1
|
||||||
with:
|
with:
|
||||||
dockerfile: build/dockerfile-prod
|
dockerfile: build/docker/prod
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ github.token }}
|
password: ${{ github.token }}
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
@ -75,7 +124,7 @@ jobs:
|
|||||||
|
|
||||||
container-vulnerability-scan:
|
container-vulnerability-scan:
|
||||||
runs-on: ubuntu-18.04
|
runs-on: ubuntu-18.04
|
||||||
needs: [container-prod]
|
needs: container-prod
|
||||||
steps:
|
steps:
|
||||||
- name: Source checkout
|
- name: Source checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
@ -89,7 +138,7 @@ jobs:
|
|||||||
- uses: anchore/scan-action@master
|
- uses: anchore/scan-action@master
|
||||||
with:
|
with:
|
||||||
image-reference: "${{ env.REGISTRY }}/${{ github.repository }}/${{ env.IMAGE }}:${{ steps.vars.outputs.sha_short }}"
|
image-reference: "${{ env.REGISTRY }}/${{ github.repository }}/${{ env.IMAGE }}:${{ steps.vars.outputs.sha_short }}"
|
||||||
dockerfile-path: "./build/dockerfile-prod"
|
dockerfile-path: "./build/docker/prod"
|
||||||
fail-build: false
|
fail-build: false
|
||||||
- name: anchore inline scan JSON results
|
- name: anchore inline scan JSON results
|
||||||
run: for j in `ls ./anchore-reports/*.json`; do echo "---- ${j} ----"; cat ${j}; echo; done
|
run: for j in `ls ./anchore-reports/*.json`; do echo "---- ${j} ----"; cat ${j}; echo; done
|
||||||
@ -98,9 +147,9 @@ jobs:
|
|||||||
name: anchore-reports
|
name: anchore-reports
|
||||||
path: ./anchore-reports/
|
path: ./anchore-reports/
|
||||||
|
|
||||||
container-test: # TODO Implement proper test
|
container-test:
|
||||||
runs-on: ubuntu-18.04
|
runs-on: ubuntu-18.04
|
||||||
needs: [container-prod]
|
needs: container-prod
|
||||||
steps:
|
steps:
|
||||||
- name: Source checkout
|
- name: Source checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
@ -112,7 +161,7 @@ jobs:
|
|||||||
- name: Docker Login
|
- name: Docker Login
|
||||||
run: docker login $REGISTRY -u $GITHUB_ACTOR -p $GITHUB_TOKEN
|
run: docker login $REGISTRY -u $GITHUB_ACTOR -p $GITHUB_TOKEN
|
||||||
- name: Docker Run Test
|
- name: Docker Run Test
|
||||||
run: docker run $REGISTRY/$GITHUB_REPOSITORY/$IMAGE:${{ steps.vars.outputs.sha_short }} /bin/sh -c "ls -la ./app"
|
run: echo "replace me with real test"
|
||||||
|
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-18.04
|
runs-on: ubuntu-18.04
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -28,6 +28,7 @@ key.json
|
|||||||
.keys/*
|
.keys/*
|
||||||
|
|
||||||
cockroach-data/*
|
cockroach-data/*
|
||||||
|
.build/
|
||||||
|
|
||||||
#binaries
|
#binaries
|
||||||
cmd/zitadel/zitadel
|
cmd/zitadel/zitadel
|
5
build/build.md
Normal file
5
build/build.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Build
|
||||||
|
|
||||||
|
## Console
|
||||||
|
|
||||||
|
## Docker
|
46
build/console/generate-grpc.sh
Executable file
46
build/console/generate-grpc.sh
Executable file
@ -0,0 +1,46 @@
|
|||||||
|
#! /bin/sh
|
||||||
|
|
||||||
|
set -eux
|
||||||
|
|
||||||
|
GEN_PATH=${GOPATH}/src/github.com/caos/zitadel/console/src/app/proto/generated
|
||||||
|
|
||||||
|
echo "Remove old files"
|
||||||
|
rm -rf $GEN_PATH
|
||||||
|
|
||||||
|
echo "Create folders"
|
||||||
|
mkdir -p $GEN_PATH
|
||||||
|
|
||||||
|
echo "Generate grpc"
|
||||||
|
|
||||||
|
protoc \
|
||||||
|
-I=/usr/local/include \
|
||||||
|
-I=${GOPATH}/src/github.com/caos/zitadel/pkg/management/api/proto \
|
||||||
|
-I=${GOPATH}/src/github.com/caos/zitadel/pkg/auth/api/proto \
|
||||||
|
-I=${GOPATH}/src/github.com/caos/zitadel/pkg/admin/api/proto \
|
||||||
|
-I=${GOPATH}/src/github.com/caos/zitadel/internal/protoc/protoc-gen-authoption \
|
||||||
|
-I=${GOPATH}/src/github.com/caos/zitadel/console/node_modules/google-proto-files \
|
||||||
|
-I=${GOPATH}/src/github.com/envoyproxy/protoc-gen-validate \
|
||||||
|
-I=${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway \
|
||||||
|
--js_out=import_style=commonjs,binary:$GEN_PATH \
|
||||||
|
--grpc-web_out=import_style=commonjs+dts,mode=grpcweb:$GEN_PATH \
|
||||||
|
${GOPATH}/src/github.com/caos/zitadel/pkg/management/api/proto/*.proto \
|
||||||
|
${GOPATH}/src/github.com/caos/zitadel/pkg/admin/api/proto/*.proto \
|
||||||
|
${GOPATH}/src/github.com/caos/zitadel/pkg/auth/api/proto/*.proto
|
||||||
|
|
||||||
|
echo "Generate annotations js file (compatibility)"
|
||||||
|
|
||||||
|
mkdir -p $GEN_PATH/google/api/
|
||||||
|
touch $GEN_PATH/google/api/annotations_pb.js
|
||||||
|
echo "export {}" > $GEN_PATH/google/api/annotations_pb.d.ts
|
||||||
|
|
||||||
|
mkdir -p $GEN_PATH/validate
|
||||||
|
touch $GEN_PATH/validate/validate_pb.js
|
||||||
|
echo "export {}" > $GEN_PATH/validate/validate_pb.d.ts
|
||||||
|
|
||||||
|
mkdir -p $GEN_PATH/protoc-gen-swagger/options
|
||||||
|
touch $GEN_PATH/protoc-gen-swagger/options/annotations_pb.js
|
||||||
|
echo "export {}" > $GEN_PATH/protoc-gen-swagger/options/annotations_pb.d.ts
|
||||||
|
|
||||||
|
mkdir -p $GEN_PATH/authoption
|
||||||
|
touch $GEN_PATH/authoption/options_pb.js
|
||||||
|
echo "export {}" > $GEN_PATH/authoption/options_pb.d.ts
|
5
build/console/generate-static.sh
Executable file
5
build/console/generate-static.sh
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#! /bin/sh
|
||||||
|
|
||||||
|
set -eux
|
||||||
|
|
||||||
|
go generate pkg/console/console.go
|
18
build/docker/prod
Normal file
18
build/docker/prod
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
#
|
||||||
|
FROM alpine:latest as prepare
|
||||||
|
RUN adduser -D zitadel
|
||||||
|
COPY .build/go/zitadel /
|
||||||
|
COPY cmd/zitadel/*.yaml /
|
||||||
|
RUN chmod a+x /zitadel
|
||||||
|
|
||||||
|
#
|
||||||
|
FROM scratch as final
|
||||||
|
COPY --from=prepare /etc/passwd /etc/passwd
|
||||||
|
COPY --from=prepare / /
|
||||||
|
|
||||||
|
USER zitadel
|
||||||
|
|
||||||
|
HEALTHCHECK NONE
|
||||||
|
|
||||||
|
ENTRYPOINT ["/zitadel"]
|
||||||
|
## TODO enable CMD
|
@ -1,4 +0,0 @@
|
|||||||
FROM alpine:latest
|
|
||||||
|
|
||||||
COPY .build/angular /app/console
|
|
||||||
COPY .build/go /app
|
|
@ -1,10 +0,0 @@
|
|||||||
# FROM sratch
|
|
||||||
FROM alpine:latest
|
|
||||||
|
|
||||||
RUN addgroup -S zitadel && adduser -S zitadel -G zitadel
|
|
||||||
|
|
||||||
USER zitadel
|
|
||||||
|
|
||||||
COPY .build/angular /app/console
|
|
||||||
COPY .build/go /app
|
|
||||||
|
|
13
console/.editorconfig
Normal file
13
console/.editorconfig
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Editor configuration, see https://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
max_line_length = off
|
||||||
|
trim_trailing_whitespace = false
|
45
console/.gitignore
vendored
Normal file
45
console/.gitignore
vendored
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# compiled output
|
||||||
|
dist/
|
||||||
|
tmp/
|
||||||
|
out-tsc/
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# profiling files
|
||||||
|
chrome-profiler-events*.json
|
||||||
|
speed-measure-plugin*.json
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.history/*
|
||||||
|
|
||||||
|
# misc
|
||||||
|
/.sass-cache
|
||||||
|
/connect.lock
|
||||||
|
/coverage
|
||||||
|
/libpeerconnection.log
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
testem.log
|
||||||
|
/typings
|
||||||
|
|
||||||
|
# System Files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
.vscode/settings.json
|
5
console/.prettierrc
Normal file
5
console/.prettierrc
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 125,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
27
console/README.md
Normal file
27
console/README.md
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Console
|
||||||
|
|
||||||
|
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.3.20.
|
||||||
|
|
||||||
|
## Development server
|
||||||
|
|
||||||
|
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
|
||||||
|
|
||||||
|
## Code scaffolding
|
||||||
|
|
||||||
|
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
|
||||||
|
|
||||||
|
## Running unit tests
|
||||||
|
|
||||||
|
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||||
|
|
||||||
|
## Running end-to-end tests
|
||||||
|
|
||||||
|
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
|
||||||
|
|
||||||
|
## Further help
|
||||||
|
|
||||||
|
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
|
137
console/angular.json
Normal file
137
console/angular.json
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||||
|
"version": 1,
|
||||||
|
"newProjectRoot": "projects",
|
||||||
|
"projects": {
|
||||||
|
"console": {
|
||||||
|
"projectType": "application",
|
||||||
|
"schematics": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"style": "scss"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"prefix": "app",
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular-devkit/build-angular:browser",
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/console",
|
||||||
|
"index": "src/index.html",
|
||||||
|
"main": "src/main.ts",
|
||||||
|
"polyfills": "src/polyfills.ts",
|
||||||
|
"tsConfig": "tsconfig.app.json",
|
||||||
|
"aot": true,
|
||||||
|
"assets": [
|
||||||
|
"src/favicon.ico",
|
||||||
|
"src/assets",
|
||||||
|
"src/manifest.webmanifest",
|
||||||
|
"src/404.html"
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.scss"
|
||||||
|
],
|
||||||
|
"scripts": []
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.prod.ts"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"optimization": true,
|
||||||
|
"outputHashing": "all",
|
||||||
|
"sourceMap": false,
|
||||||
|
"extractCss": true,
|
||||||
|
"namedChunks": false,
|
||||||
|
"extractLicenses": true,
|
||||||
|
"vendorChunk": false,
|
||||||
|
"buildOptimizer": true,
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "4mb",
|
||||||
|
"maximumError": "5mb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "6kb",
|
||||||
|
"maximumError": "10kb"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"serviceWorker": true,
|
||||||
|
"ngswConfigPath": "ngsw-config.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
|
"options": {
|
||||||
|
"browserTarget": "console:build"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"browserTarget": "console:build:production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extract-i18n": {
|
||||||
|
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||||
|
"options": {
|
||||||
|
"browserTarget": "console:build"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"builder": "@angular-devkit/build-angular:karma",
|
||||||
|
"options": {
|
||||||
|
"main": "src/test.ts",
|
||||||
|
"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": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"builder": "@angular-devkit/build-angular:tslint",
|
||||||
|
"options": {
|
||||||
|
"tsConfig": [
|
||||||
|
"tsconfig.app.json",
|
||||||
|
"tsconfig.spec.json"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"**/node_modules/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"e2e": {
|
||||||
|
"builder": "@angular-devkit/build-angular:protractor",
|
||||||
|
"options": {
|
||||||
|
"protractorConfig": "e2e/protractor.conf.js",
|
||||||
|
"devServerTarget": "console:serve"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"devServerTarget": "console:serve:production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultProject": "console",
|
||||||
|
"cli": {
|
||||||
|
"analytics": "2b4e8e6c-f053-4562-b7a6-00c6c06a6791"
|
||||||
|
}
|
||||||
|
}
|
12
console/browserslist
Normal file
12
console/browserslist
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||||
|
# For additional information regarding the format and rule options, please see:
|
||||||
|
# https://github.com/browserslist/browserslist#queries
|
||||||
|
|
||||||
|
# You can see what browsers were selected by your queries by running:
|
||||||
|
# npx browserslist
|
||||||
|
|
||||||
|
> 0.5%
|
||||||
|
last 2 versions
|
||||||
|
Firefox ESR
|
||||||
|
not dead
|
||||||
|
not IE 9-11 # For IE 9-11 support, remove 'not'.
|
32
console/e2e/protractor.conf.js
Normal file
32
console/e2e/protractor.conf.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// @ts-check
|
||||||
|
// Protractor configuration file, see link for more information
|
||||||
|
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||||
|
|
||||||
|
const { SpecReporter } = require('jasmine-spec-reporter');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type { import("protractor").Config }
|
||||||
|
*/
|
||||||
|
exports.config = {
|
||||||
|
allScriptsTimeout: 11000,
|
||||||
|
specs: [
|
||||||
|
'./src/**/*.e2e-spec.ts'
|
||||||
|
],
|
||||||
|
capabilities: {
|
||||||
|
browserName: 'chrome'
|
||||||
|
},
|
||||||
|
directConnect: true,
|
||||||
|
baseUrl: 'http://localhost:4200/',
|
||||||
|
framework: 'jasmine',
|
||||||
|
jasmineNodeOpts: {
|
||||||
|
showColors: true,
|
||||||
|
defaultTimeoutInterval: 30000,
|
||||||
|
print: function() {}
|
||||||
|
},
|
||||||
|
onPrepare() {
|
||||||
|
require('ts-node').register({
|
||||||
|
project: require('path').join(__dirname, './tsconfig.json')
|
||||||
|
});
|
||||||
|
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
|
||||||
|
}
|
||||||
|
};
|
26
console/e2e/src/app.e2e-spec.ts
Normal file
26
console/e2e/src/app.e2e-spec.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { browser, logging } from 'protractor';
|
||||||
|
|
||||||
|
import { AppPage } from './app.po';
|
||||||
|
|
||||||
|
describe('workspace-project App', () => {
|
||||||
|
let page: AppPage;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
page = new AppPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display welcome message', () => {
|
||||||
|
page.navigateTo();
|
||||||
|
expect(page.getTitleText()).toEqual('console app is running!');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
// Assert that there are no errors emitted from the browser
|
||||||
|
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
|
||||||
|
expect(logs).not.toContain(
|
||||||
|
jasmine.objectContaining({
|
||||||
|
level: logging.Level.SEVERE
|
||||||
|
} as logging.Entry)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
11
console/e2e/src/app.po.ts
Normal file
11
console/e2e/src/app.po.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { browser, by, element } from 'protractor';
|
||||||
|
|
||||||
|
export class AppPage {
|
||||||
|
navigateTo() {
|
||||||
|
return browser.get(browser.baseUrl) as Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTitleText() {
|
||||||
|
return element(by.css('app-root .content span')).getText() as Promise<string>;
|
||||||
|
}
|
||||||
|
}
|
13
console/e2e/tsconfig.json
Normal file
13
console/e2e/tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../out-tsc/e2e",
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es5",
|
||||||
|
"types": [
|
||||||
|
"jasmine",
|
||||||
|
"jasminewd2",
|
||||||
|
"node"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
32
console/karma.conf.js
Normal file
32
console/karma.conf.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// Karma configuration file, see link for more information
|
||||||
|
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||||
|
|
||||||
|
module.exports = function(config) {
|
||||||
|
config.set({
|
||||||
|
basePath: '',
|
||||||
|
frameworks: [ 'jasmine', '@angular-devkit/build-angular' ],
|
||||||
|
plugins: [
|
||||||
|
require('karma-jasmine'),
|
||||||
|
require('karma-chrome-launcher'),
|
||||||
|
require('karma-jasmine-html-reporter'),
|
||||||
|
require('karma-coverage-istanbul-reporter'),
|
||||||
|
require('@angular-devkit/build-angular/plugins/karma')
|
||||||
|
],
|
||||||
|
client: {
|
||||||
|
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||||
|
},
|
||||||
|
coverageIstanbulReporter: {
|
||||||
|
dir: require('path').join(__dirname, './coverage/console'),
|
||||||
|
reports: [ 'html', 'lcovonly', 'text-summary' ],
|
||||||
|
fixWebpackSourcePaths: true
|
||||||
|
},
|
||||||
|
reporters: [ 'progress', 'kjhtml' ],
|
||||||
|
port: 9876,
|
||||||
|
colors: true,
|
||||||
|
logLevel: config.LOG_INFO,
|
||||||
|
autoWatch: true,
|
||||||
|
browsers: [ 'Chrome' ],
|
||||||
|
singleRun: false,
|
||||||
|
restartOnFileChange: true
|
||||||
|
});
|
||||||
|
};
|
43
console/ngsw-config.json
Normal file
43
console/ngsw-config.json
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
|
||||||
|
"index": "/index.html",
|
||||||
|
"assetGroups": [
|
||||||
|
{
|
||||||
|
"name": "app",
|
||||||
|
"installMode": "prefetch",
|
||||||
|
"resources": {
|
||||||
|
"files": [
|
||||||
|
"/favicon.ico",
|
||||||
|
"/index.html",
|
||||||
|
"/manifest.webmanifest",
|
||||||
|
"/*.css",
|
||||||
|
"/*.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"name": "assets",
|
||||||
|
"installMode": "lazy",
|
||||||
|
"updateMode": "prefetch",
|
||||||
|
"resources": {
|
||||||
|
"files": [
|
||||||
|
"/assets/**",
|
||||||
|
"/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dataGroups": [
|
||||||
|
{
|
||||||
|
"name": "api-freshness",
|
||||||
|
"urls": [
|
||||||
|
"/GetMyzitadelPermissions"
|
||||||
|
],
|
||||||
|
"cacheConfig": {
|
||||||
|
"strategy": "freshness",
|
||||||
|
"maxSize": 100,
|
||||||
|
"maxAge": "1d",
|
||||||
|
"timeout": "0s"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
19886
console/package-lock.json
generated
Normal file
19886
console/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
71
console/package.json
Normal file
71
console/package.json
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
{
|
||||||
|
"name": "console",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"ng": "ng",
|
||||||
|
"prestart": "npm run proto",
|
||||||
|
"start": "ng serve",
|
||||||
|
"build": "ng build",
|
||||||
|
"prodbuild": "ng build --prod",
|
||||||
|
"test": "ng test",
|
||||||
|
"lint": "ng lint && stylelint './projects/**/*.scss' --syntax scss",
|
||||||
|
"e2e": "ng e2e",
|
||||||
|
"proto": "./etc/generate-grpc.sh || echo 'could not generate grpc'"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@angular/animations": "~9.1.0",
|
||||||
|
"@angular/cdk": "~9.0.1",
|
||||||
|
"@angular/common": "~9.1.0",
|
||||||
|
"@angular/compiler": "~9.1.0",
|
||||||
|
"@angular/core": "~9.1.0",
|
||||||
|
"@angular/forms": "~9.1.0",
|
||||||
|
"@angular/material": "^9.0.1",
|
||||||
|
"@angular/platform-browser": "~9.1.0",
|
||||||
|
"@angular/platform-browser-dynamic": "~9.1.0",
|
||||||
|
"@angular/router": "~9.1.0",
|
||||||
|
"@angular/service-worker": "~9.1.0",
|
||||||
|
"@ngx-translate/core": "^12.1.2",
|
||||||
|
"@ngx-translate/http-loader": "^4.0.0",
|
||||||
|
"@types/google-protobuf": "^3.7.2",
|
||||||
|
"@types/uuid": "^7.0.0",
|
||||||
|
"angular-oauth2-oidc": "^8.0.4",
|
||||||
|
"angularx-qrcode": "^2.1.0",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"google-proto-files": "^1.1.1",
|
||||||
|
"google-protobuf": "^3.11.4",
|
||||||
|
"grpc": "^1.24.2",
|
||||||
|
"grpc-web": "^1.0.7",
|
||||||
|
"hammerjs": "^2.0.8",
|
||||||
|
"moment": "^2.24.0",
|
||||||
|
"ngx-moment": "^3.5.0",
|
||||||
|
"prettier-stylelint": "^0.4.2",
|
||||||
|
"rxjs": "~6.5.4",
|
||||||
|
"ts-protoc-gen": "^0.12.0",
|
||||||
|
"tslib": "^1.10.0",
|
||||||
|
"uuid": "^7.0.1",
|
||||||
|
"zone.js": "~0.10.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@angular-devkit/build-angular": "~0.901.0",
|
||||||
|
"@angular/cli": "~9.1.0",
|
||||||
|
"@angular/compiler-cli": "~9.1.0",
|
||||||
|
"@angular/language-service": "~9.1.0",
|
||||||
|
"@types/jasmine": "~3.5.5",
|
||||||
|
"@types/jasminewd2": "~2.0.3",
|
||||||
|
"@types/node": "^13.7.4",
|
||||||
|
"codelyzer": "^5.1.2",
|
||||||
|
"jasmine-core": "~3.5.0",
|
||||||
|
"jasmine-spec-reporter": "~4.2.1",
|
||||||
|
"karma": "~4.4.1",
|
||||||
|
"karma-chrome-launcher": "~3.1.0",
|
||||||
|
"karma-coverage-istanbul-reporter": "~2.1.1",
|
||||||
|
"karma-jasmine": "~3.1.1",
|
||||||
|
"karma-jasmine-html-reporter": "^1.4.0",
|
||||||
|
"prettier": "^1.19.1",
|
||||||
|
"protractor": "~5.4.0",
|
||||||
|
"ts-node": "~8.6.2",
|
||||||
|
"tslint": "~6.1.0",
|
||||||
|
"typescript": "^3.7.5"
|
||||||
|
}
|
||||||
|
}
|
23
console/src/404.html
Normal file
23
console/src/404.html
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
|
||||||
|
<title>caos console</title>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
sessionStorage.redirect = location.href;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<meta http-equiv="refresh" content="0;URL='/'"></meta>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
|
||||||
|
</html>
|
56
console/src/app/app-routing.module.ts
Normal file
56
console/src/app/app-routing.module.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
|
||||||
|
import { AuthGuard } from './guards/auth.guard';
|
||||||
|
import { RoleGuard } from './guards/role.guard';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
loadChildren: () => import('./pages/home/home.module').then(m => m.HomeModule),
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'projects',
|
||||||
|
loadChildren: () => import('./pages/projects/projects.module').then(m => m.ProjectsModule),
|
||||||
|
canActivate: [AuthGuard, RoleGuard],
|
||||||
|
data: {
|
||||||
|
roles: ['project.read'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'user',
|
||||||
|
loadChildren: () => import('./pages/user-detail/user-detail.module').then(m => m.UserDetailModule),
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'users',
|
||||||
|
loadChildren: () => import('./pages/user-list/user-list.module').then(m => m.UserListModule),
|
||||||
|
canActivate: [AuthGuard, RoleGuard],
|
||||||
|
data: {
|
||||||
|
roles: ['user.read'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'orgs',
|
||||||
|
loadChildren: () => import('./pages/orgs/orgs.module').then(m => m.OrgsModule),
|
||||||
|
canActivate: [AuthGuard, RoleGuard],
|
||||||
|
data: {
|
||||||
|
roles: ['org.read'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'signedout',
|
||||||
|
loadChildren: () => import('./pages/signedout/signedout.module').then(m => m.SignedoutModule),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '**',
|
||||||
|
redirectTo: '/',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forRoot(routes)],
|
||||||
|
exports: [RouterModule],
|
||||||
|
})
|
||||||
|
export class AppRoutingModule { }
|
101
console/src/app/app.component.html
Normal file
101
console/src/app/app.component.html
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
<mat-toolbar class="root-header">
|
||||||
|
<button aria-label="Toggle sidenav" mat-icon-button (click)="drawer.toggle()">
|
||||||
|
<mat-icon aria-label="Side nav toggle icon">menu</mat-icon>
|
||||||
|
</button>
|
||||||
|
<a *ngIf="(isHandset$ | async) == false" class="title ailerons" [routerLink]="['/']">
|
||||||
|
<img class="logo" *ngIf="componentCssClass == 'dark-theme'; else lighttheme"
|
||||||
|
src="../assets/images/zitadel-logo-oneline-darkdesign.svg" />
|
||||||
|
<ng-template #lighttheme>
|
||||||
|
<img class="logo" src="../assets/images/zitadel-logo-oneline-lightdesign.svg" />
|
||||||
|
</ng-template>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<button (click)="loadOrgs()" *ngIf="profile?.id && org" mat-button
|
||||||
|
[matMenuTriggerFor]="menu">{{org?.name ? org.name : 'NO NAME'}}
|
||||||
|
<mat-icon>
|
||||||
|
arrow_drop_down</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<mat-menu #menu="matMenu">
|
||||||
|
<mat-progress-bar *ngIf="orgLoading" color="accent" mode="indeterminate"></mat-progress-bar>
|
||||||
|
<button class="show-all" mat-menu-item [routerLink]="[ '/orgs' ]">Show all organizations</button>
|
||||||
|
|
||||||
|
<button [ngClass]="{'active': temporg.id === org?.id}" [disabled]="!temporg.id" *ngFor="let temporg of orgs"
|
||||||
|
mat-menu-item (click)="setActiveOrg(temporg)">
|
||||||
|
<mat-icon class="avatar">business</mat-icon>
|
||||||
|
{{temporg?.name ? temporg.name : 'NO NAME'}}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ng-template appHasRole [appHasRole]="['iam.write']">
|
||||||
|
<button mat-menu-item [routerLink]="[ '/orgs/create' ]">
|
||||||
|
<mat-icon class="avatar">add</mat-icon>
|
||||||
|
{{'MENU.NEWORG' | translate}}
|
||||||
|
</button>
|
||||||
|
</ng-template>
|
||||||
|
</mat-menu>
|
||||||
|
<span class="fill-space"></span>
|
||||||
|
|
||||||
|
<div matTooltip="IAM user" class="iamreadwrite"></div>
|
||||||
|
<div (clickOutside)="closeAccountCard()" class="icon-container">
|
||||||
|
<div class="avatar-wrapper dontcloseonclick" (click)="showAccount = !showAccount">
|
||||||
|
<div class="avatar-circle dontcloseonclick" [ngClass]="{'active': showAccount}">
|
||||||
|
<img class="avatar dontcloseonclick" *ngIf="componentCssClass == 'dark-theme'; else lighttheme"
|
||||||
|
src="../assets/images/account-circle-outline.png" />
|
||||||
|
<ng-template #lighttheme>
|
||||||
|
<img class="avatar dontcloseonclick" src="../assets/images/account-circle-outline-dark.png" />
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<app-accounts-card @accounts class="a_card mat-elevation-z5" *ngIf="showAccount" (close)="showAccount = false"
|
||||||
|
[profile]="profile" [iamuser]="iamreadwrite">
|
||||||
|
</app-accounts-card>
|
||||||
|
</div>
|
||||||
|
</mat-toolbar>
|
||||||
|
|
||||||
|
<mat-drawer-container *ngIf="(authService.user | async) || {} as user" class="main-container">
|
||||||
|
<mat-drawer #drawer class="side" [mode]="(isHandset$ | async) ? 'over' : 'side'" [opened]="!(isHandset$ | async)">
|
||||||
|
<div class="side-column">
|
||||||
|
<div class="list">
|
||||||
|
<a *ngIf="authService.authenticationChanged | async" class="nav-item" [routerLinkActive]="['active']"
|
||||||
|
[routerLinkActiveOptions]="{ exact: true }" [routerLink]="['/user/me']">
|
||||||
|
<mat-icon class="icon" svgIcon="mdi_account_circle_outline"></mat-icon>
|
||||||
|
<span class="label">{{ 'MENU.PERSONAL_INFO' | translate }}</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a *ngIf="showOrgSection && org?.id" class="nav-item" [routerLinkActive]="['active']"
|
||||||
|
[routerLink]="[ '/orgs', org.id]">
|
||||||
|
<mat-icon class="icon">business</mat-icon>
|
||||||
|
<span class="label">{{org?.name ? org.name : 'MENU.ORGANIZATION' | translate}}</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a *ngIf="showProjectSection" class="nav-item" [routerLinkActive]="['active']"
|
||||||
|
[routerLink]="[ '/projects']">
|
||||||
|
<mat-icon class="icon">folder_open</mat-icon>
|
||||||
|
<span class="label">{{ 'MENU.PROJECT' | translate }}</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a *ngIf="showUserSection" class="nav-item" [routerLinkActive]="['active']" [routerLink]="[ '/users']"
|
||||||
|
[routerLinkActiveOptions]="{ exact: true }">
|
||||||
|
<mat-icon class="icon">people_outline</mat-icon>
|
||||||
|
<span class="label">{{ 'MENU.USER' | translate }}</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<span class="fill-space"></span>
|
||||||
|
<a class="nav-item" (click)="authService.signout()">
|
||||||
|
<mat-icon class="icon" svgIcon="mdi_logout"></mat-icon>
|
||||||
|
<span class="label">{{ 'MENU.LOGOUT' | translate }}</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<span class="fill-space"></span>
|
||||||
|
<div class="footer">
|
||||||
|
<a href="https://caos.ch/impressum/" target="_blank" rel="noreferrer">AGB</a>
|
||||||
|
<a href="https://caos.ch/impressum/" target="_blank" rel="noreferrer">Impressum</a></div>
|
||||||
|
</div>
|
||||||
|
</mat-drawer>
|
||||||
|
<mat-drawer-content class="content">
|
||||||
|
<div class="router" [@routeAnimations]="prepareRoute(outlet)">
|
||||||
|
<router-outlet #outlet="outlet"></router-outlet>
|
||||||
|
</div>
|
||||||
|
</mat-drawer-content>
|
||||||
|
</mat-drawer-container>
|
247
console/src/app/app.component.scss
Normal file
247
console/src/app/app.component.scss
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
|
||||||
|
.root-header {
|
||||||
|
position: relative;
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
height: 60px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 1rem;
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 40px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
text-decoration: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 400;
|
||||||
|
margin-left: 1rem;
|
||||||
|
line-height: 1.2rem;
|
||||||
|
font-family: 'Rubik';
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu {
|
||||||
|
border-radius: .5rem;
|
||||||
|
background-color: #2d2e30;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill-space {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iamreadwrite {
|
||||||
|
height: 8px;
|
||||||
|
width: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(to bottom right, rgb(240,140,53), rgb(233, 60, 231));
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
position: relative;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
.docs {
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-family: 'ailerons', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
.avatar-circle {
|
||||||
|
height: 35px;
|
||||||
|
width: 35px;
|
||||||
|
background-color: transparent;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: background-color .2s ease-in;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
display: block;
|
||||||
|
margin: auto auto;
|
||||||
|
height: 30px;
|
||||||
|
width: 30px;
|
||||||
|
line-height: 35px;
|
||||||
|
font-size: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
text-align: center;
|
||||||
|
fill: white;
|
||||||
|
|
||||||
|
* {
|
||||||
|
fill: white;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &.active {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #ffffff20;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.a_card {
|
||||||
|
position: absolute;
|
||||||
|
top: 60px;
|
||||||
|
right: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: calc(100vh - 60px);
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.side {
|
||||||
|
width: 300px;
|
||||||
|
border-right: none;
|
||||||
|
|
||||||
|
.side-column {
|
||||||
|
height: calc(100vh - 70px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
|
||||||
|
.list {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
margin-top: 2rem;
|
||||||
|
|
||||||
|
.logout-icon {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill-space {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.2rem 1rem;
|
||||||
|
// color: inherit;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
padding-right: 2rem;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-family: 'Rubik';
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: .9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #00000010;
|
||||||
|
border-top-right-radius: 1.5rem;
|
||||||
|
border-bottom-right-radius: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-top-right-radius: 1.5rem;
|
||||||
|
border-bottom-right-radius: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-status {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill-space {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-button {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-button {
|
||||||
|
margin: 1rem;
|
||||||
|
border-radius: 1.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.router {
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-section {
|
||||||
|
display: block;
|
||||||
|
padding: 0 .5rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
align-self: flex-start;
|
||||||
|
border-radius: 1rem;
|
||||||
|
|
||||||
|
.round-light {
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 50%;
|
||||||
|
height: 30px;
|
||||||
|
width: 30px;
|
||||||
|
margin: .5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
background: linear-gradient(315deg, #e6e6e6, #ffffff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.round-dark {
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 50%;
|
||||||
|
height: 30px;
|
||||||
|
width: 30px;
|
||||||
|
margin: .5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
background: linear-gradient(315deg, #000000, #000000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
padding: 1rem;
|
||||||
|
a {
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #81868a;
|
||||||
|
font-size: .8rem;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 1px;
|
||||||
|
font-weight: 300;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
console/src/app/app.component.spec.ts
Normal file
32
console/src/app/app.component.spec.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { async, TestBed } from '@angular/core/testing';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
|
||||||
|
describe('AppComponent', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [RouterTestingModule],
|
||||||
|
declarations: [AppComponent],
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should create the app', () => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
const app = fixture.debugElement.componentInstance;
|
||||||
|
expect(app).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should have as title 'console'`, () => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
const app = fixture.debugElement.componentInstance;
|
||||||
|
expect(app.title).toEqual('console');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render title', () => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
const compiled = fixture.debugElement.nativeElement;
|
||||||
|
expect(compiled.querySelector('.content span').textContent).toContain('console app is running!');
|
||||||
|
});
|
||||||
|
});
|
299
console/src/app/app.component.ts
Normal file
299
console/src/app/app.component.ts
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
import { animate, group, query, style, transition, trigger } from '@angular/animations';
|
||||||
|
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
|
||||||
|
import { OverlayContainer } from '@angular/cdk/overlay';
|
||||||
|
import { Component, HostBinding, OnDestroy, ViewChild } from '@angular/core';
|
||||||
|
import { MatIconRegistry } from '@angular/material/icon';
|
||||||
|
import { MatDrawer } from '@angular/material/sidenav';
|
||||||
|
import { DomSanitizer } from '@angular/platform-browser';
|
||||||
|
import { Router, RouterOutlet } from '@angular/router';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { Observable, of, Subscription } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { Org, UserProfile } from './proto/generated/auth_pb';
|
||||||
|
import { AuthUserService } from './services/auth-user.service';
|
||||||
|
import { AuthService } from './services/auth.service';
|
||||||
|
import { ThemeService } from './services/theme.service';
|
||||||
|
import { ToastService } from './services/toast.service';
|
||||||
|
import { UpdateService } from './services/update.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
templateUrl: './app.component.html',
|
||||||
|
styleUrls: ['./app.component.scss'],
|
||||||
|
animations: [
|
||||||
|
trigger('accounts', [
|
||||||
|
transition(':enter', [
|
||||||
|
style({
|
||||||
|
transform: 'scale(.9) translateY(-10%)',
|
||||||
|
height: '200px',
|
||||||
|
opacity: 0,
|
||||||
|
}),
|
||||||
|
animate(
|
||||||
|
'.1s ease-out',
|
||||||
|
style({
|
||||||
|
transform: 'scale(1) translateY(0%)',
|
||||||
|
height: '*',
|
||||||
|
opacity: 1,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
trigger('routeAnimations', [
|
||||||
|
transition('HomePage => AddPage', [
|
||||||
|
style({ transform: 'translateX(100%)' }),
|
||||||
|
animate('250ms ease-in-out', style({ transform: 'translateX(0%)' })),
|
||||||
|
]),
|
||||||
|
transition('AddPage => HomePage', [animate('250ms', style({ transform: 'translateX(100%)' }))]),
|
||||||
|
transition('HomePage => DetailPage', [
|
||||||
|
query(':enter, :leave', style({ position: 'absolute', left: 0, right: 0 }), {
|
||||||
|
optional: true,
|
||||||
|
}),
|
||||||
|
group([
|
||||||
|
query(
|
||||||
|
':enter',
|
||||||
|
[
|
||||||
|
style({
|
||||||
|
transform: 'translateX(20%)',
|
||||||
|
opacity: 0.5,
|
||||||
|
}),
|
||||||
|
animate(
|
||||||
|
'.35s ease-in',
|
||||||
|
style({
|
||||||
|
transform: 'translateX(0%)',
|
||||||
|
opacity: 1,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
{
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
query(
|
||||||
|
':leave',
|
||||||
|
[style({ opacity: 1, width: '100%' }), animate('.35s ease-out', style({ opacity: 0 }))],
|
||||||
|
{
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
transition('DetailPage => HomePage', [
|
||||||
|
query(':enter, :leave', style({ position: 'absolute', left: 0, right: 0 }), {
|
||||||
|
optional: true,
|
||||||
|
}),
|
||||||
|
group([
|
||||||
|
query(
|
||||||
|
':enter',
|
||||||
|
[
|
||||||
|
style({
|
||||||
|
opacity: 0,
|
||||||
|
}),
|
||||||
|
animate(
|
||||||
|
'.35s ease-out',
|
||||||
|
style({
|
||||||
|
opacity: 1,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
{
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
query(
|
||||||
|
':leave',
|
||||||
|
[
|
||||||
|
style({ width: '100%', transform: 'translateX(0%)' }),
|
||||||
|
animate('.35s ease-in', style({ transform: 'translateX(30%)', opacity: 0 })),
|
||||||
|
],
|
||||||
|
{
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AppComponent implements OnDestroy {
|
||||||
|
@ViewChild('drawer')
|
||||||
|
public drawer!: MatDrawer;
|
||||||
|
public isHandset$: Observable<boolean> = this.breakpointObserver
|
||||||
|
.observe(Breakpoints.Handset)
|
||||||
|
.pipe(map(result => {
|
||||||
|
return result.matches;
|
||||||
|
}));
|
||||||
|
@HostBinding('class') public componentCssClass: string = 'dark-theme';
|
||||||
|
|
||||||
|
public showAccount: boolean = false;
|
||||||
|
public org!: Org.AsObject;
|
||||||
|
public orgs: Org.AsObject[] = [];
|
||||||
|
public profile!: UserProfile.AsObject;
|
||||||
|
public isDarkTheme: Observable<boolean> = of(true);
|
||||||
|
|
||||||
|
public orgLoading: boolean = false;
|
||||||
|
|
||||||
|
public showProjectSection: boolean = false;
|
||||||
|
public showOrgSection: boolean = false;
|
||||||
|
public showUserSection: boolean = false;
|
||||||
|
public iamreadwrite: boolean = false;
|
||||||
|
|
||||||
|
private authSub: Subscription = new Subscription();
|
||||||
|
private orgSub: Subscription = new Subscription();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public translate: TranslateService,
|
||||||
|
public authService: AuthService,
|
||||||
|
private breakpointObserver: BreakpointObserver,
|
||||||
|
public overlayContainer: OverlayContainer,
|
||||||
|
private themeService: ThemeService,
|
||||||
|
public userService: AuthUserService,
|
||||||
|
public matIconRegistry: MatIconRegistry,
|
||||||
|
public domSanitizer: DomSanitizer,
|
||||||
|
private toast: ToastService,
|
||||||
|
private router: Router,
|
||||||
|
update: UpdateService,
|
||||||
|
) {
|
||||||
|
console.log('%cWait!', 'text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black; color: #5282c1; font-size: 50px');
|
||||||
|
console.log('%cInserting something here could give attackers access to your caos account.', 'color: red; font-size: 18px');
|
||||||
|
console.log('%cIf you don\'t know exactly what you\'re doing, close the window and stay on the safe side', 'font-size: 16px');
|
||||||
|
console.log('%cIf you know exactly what you are doing, you should work for us', 'font-size: 16px');
|
||||||
|
this.setLanguage();
|
||||||
|
|
||||||
|
this.matIconRegistry.addSvgIcon(
|
||||||
|
'mdi_account_check_outline',
|
||||||
|
this.domSanitizer.bypassSecurityTrustResourceUrl('assets/mdi/account-check-outline.svg'),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.matIconRegistry.addSvgIcon(
|
||||||
|
'mdi_account_cancel',
|
||||||
|
this.domSanitizer.bypassSecurityTrustResourceUrl('assets/mdi/account-cancel-outline.svg'),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.matIconRegistry.addSvgIcon(
|
||||||
|
'mdi_logout',
|
||||||
|
this.domSanitizer.bypassSecurityTrustResourceUrl('assets/mdi/logout.svg'),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.matIconRegistry.addSvgIcon(
|
||||||
|
'mdi_light_on',
|
||||||
|
this.domSanitizer.bypassSecurityTrustResourceUrl('assets/mdi/lightbulb-on-outline.svg'),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.matIconRegistry.addSvgIcon(
|
||||||
|
'mdi_content_copy',
|
||||||
|
this.domSanitizer.bypassSecurityTrustResourceUrl('assets/mdi/content-copy.svg'),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.matIconRegistry.addSvgIcon(
|
||||||
|
'mdi_light_off',
|
||||||
|
this.domSanitizer.bypassSecurityTrustResourceUrl('assets/mdi/lightbulb-off-outline.svg'),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.matIconRegistry.addSvgIcon(
|
||||||
|
'mdi_radar',
|
||||||
|
this.domSanitizer.bypassSecurityTrustResourceUrl('assets/mdi/radar.svg'),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.matIconRegistry.addSvgIcon(
|
||||||
|
'mdi_account_circle_outline',
|
||||||
|
this.domSanitizer.bypassSecurityTrustResourceUrl('assets/mdi/account-circle-outline.svg'),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.matIconRegistry.addSvgIcon(
|
||||||
|
'mdi_lock_question',
|
||||||
|
this.domSanitizer.bypassSecurityTrustResourceUrl('assets/mdi/lock-question.svg'),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.matIconRegistry.addSvgIcon(
|
||||||
|
'mdi_textbox_password',
|
||||||
|
this.domSanitizer.bypassSecurityTrustResourceUrl('assets/mdi/textbox-password.svg'),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.matIconRegistry.addSvgIcon(
|
||||||
|
'mdi_lock_reset',
|
||||||
|
this.domSanitizer.bypassSecurityTrustResourceUrl('assets/mdi/lock-reset.svg'),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.orgSub = this.authService.activeOrgChanged.subscribe(org => {
|
||||||
|
this.org = org;
|
||||||
|
this.loadPermissions();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.authSub = this.authService.authenticationChanged.subscribe((authenticated) => {
|
||||||
|
if (authenticated) {
|
||||||
|
// this.userService.GetMyzitadelPermissions().pipe(take(1)).subscribe(perm => console.log(perm.toObject()));
|
||||||
|
this.loadPermissions();
|
||||||
|
this.authService.GetActiveOrg().then(org => {
|
||||||
|
this.org = org;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const theme = localStorage.getItem('theme');
|
||||||
|
if (theme) {
|
||||||
|
this.overlayContainer.getContainerElement().classList.add(theme);
|
||||||
|
this.componentCssClass = theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isDarkTheme = this.themeService.isDarkTheme;
|
||||||
|
this.isDarkTheme.subscribe(thema => this.onSetTheme(thema ? 'dark-theme' : 'light-theme'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy(): void {
|
||||||
|
this.authSub.unsubscribe();
|
||||||
|
this.orgSub.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
public loadPermissions(): void {
|
||||||
|
this.userService.isAllowed(['iam.read', 'iam.write'], true).subscribe(allowed => this.iamreadwrite = allowed);
|
||||||
|
this.userService.isAllowed(['org.read']).subscribe(allowed => this.showOrgSection = allowed);
|
||||||
|
this.userService.isAllowed(['project.read']).subscribe(allowed => this.showProjectSection = allowed);
|
||||||
|
this.userService.isAllowed(['user.read']).subscribe(allowed => this.showUserSection = allowed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public loadOrgs(): void {
|
||||||
|
this.orgLoading = true;
|
||||||
|
this.userService.SearchMyProjectOrgs(10, 0).then(res => {
|
||||||
|
this.orgs = res.toObject().resultList;
|
||||||
|
this.orgLoading = false;
|
||||||
|
}).catch(error => {
|
||||||
|
this.toast.showError(error.message);
|
||||||
|
this.orgLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public prepareRoute(outlet: RouterOutlet): boolean {
|
||||||
|
return outlet && outlet.activatedRouteData && outlet.activatedRouteData.animation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeAccountCard(): void {
|
||||||
|
if (this.showAccount) {
|
||||||
|
this.showAccount = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public onSetTheme(theme: string): void {
|
||||||
|
localStorage.setItem('theme', theme);
|
||||||
|
this.overlayContainer.getContainerElement().classList.add(theme);
|
||||||
|
this.componentCssClass = theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setLanguage(): void {
|
||||||
|
this.translate.addLangs(['en', 'de']);
|
||||||
|
this.translate.setDefaultLang('en');
|
||||||
|
|
||||||
|
this.authService.user.subscribe(userprofile => {
|
||||||
|
console.log(userprofile);
|
||||||
|
this.profile = userprofile;
|
||||||
|
const lang = userprofile.preferredLanguage.match(/en|de/) ? userprofile.preferredLanguage : 'en';
|
||||||
|
this.translate.use(lang);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public setActiveOrg(org: Org.AsObject): void {
|
||||||
|
this.org = org;
|
||||||
|
this.authService.setActiveOrg(org);
|
||||||
|
this.router.navigate(['/']);
|
||||||
|
}
|
||||||
|
}
|
153
console/src/app/app.module.ts
Normal file
153
console/src/app/app.module.ts
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
import { OverlayModule } from '@angular/cdk/overlay';
|
||||||
|
import { CommonModule, registerLocaleData } from '@angular/common';
|
||||||
|
import { HttpClient, HttpClientModule } from '@angular/common/http';
|
||||||
|
import localeDe from '@angular/common/locales/de';
|
||||||
|
import { APP_INITIALIZER, NgModule } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
|
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||||
|
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||||
|
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||||
|
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||||
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||||
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { ServiceWorkerModule } from '@angular/service-worker';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
|
||||||
|
import { AuthConfig, OAuthModule, OAuthStorage } from 'angular-oauth2-oidc';
|
||||||
|
|
||||||
|
import { environment } from '../environments/environment';
|
||||||
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
import { HasRoleModule } from './directives/has-role/has-role.module';
|
||||||
|
import { OutsideClickModule } from './directives/outside-click/outside-click.module';
|
||||||
|
import { AccountsCardModule } from './modules/accounts-card/accounts-card.module';
|
||||||
|
import { SignedoutComponent } from './pages/signedout/signedout.component';
|
||||||
|
import { AuthUserService } from './services/auth-user.service';
|
||||||
|
import { AuthService } from './services/auth.service';
|
||||||
|
import { GrpcAuthInterceptor } from './services/grpc-auth.interceptor';
|
||||||
|
import { GRPC_INTERCEPTORS } from './services/grpc-interceptor';
|
||||||
|
import { GrpcOrgInterceptor } from './services/grpc-org.interceptor';
|
||||||
|
import { GrpcService } from './services/grpc.service';
|
||||||
|
import { StatehandlerProcessorService, StatehandlerProcessorServiceImpl } from './services/statehandler-processor.service';
|
||||||
|
import { StatehandlerService, StatehandlerServiceImpl } from './services/statehandler.service';
|
||||||
|
import { StorageService } from './services/storage.service';
|
||||||
|
import { ThemeService } from './services/theme.service';
|
||||||
|
|
||||||
|
registerLocaleData(localeDe);
|
||||||
|
|
||||||
|
// AoT requires an exported function for factories
|
||||||
|
export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
|
||||||
|
return new TranslateHttpLoader(http);
|
||||||
|
}
|
||||||
|
|
||||||
|
const appInitializerFn = (grpcServ: GrpcService) => {
|
||||||
|
return () => {
|
||||||
|
return grpcServ.loadAppEnvironment();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const stateHandlerFn = (stateHandler: StatehandlerService) => {
|
||||||
|
return () => {
|
||||||
|
return stateHandler.initStateHandler();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const authConfig: AuthConfig = {
|
||||||
|
redirectUri: window.location.origin + '/auth/callback',
|
||||||
|
scope: 'openid profile email', // offline_access
|
||||||
|
responseType: 'code',
|
||||||
|
// showDebugInformation: true,
|
||||||
|
oidc: true,
|
||||||
|
postLogoutRedirectUri: window.location.origin + '/signedout',
|
||||||
|
};
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AppComponent,
|
||||||
|
SignedoutComponent,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
AppRoutingModule,
|
||||||
|
CommonModule,
|
||||||
|
BrowserModule,
|
||||||
|
OverlayModule,
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useFactory: HttpLoaderFactory,
|
||||||
|
deps: [HttpClient],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
AccountsCardModule,
|
||||||
|
HasRoleModule,
|
||||||
|
BrowserAnimationsModule,
|
||||||
|
HttpClientModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatTooltipModule,
|
||||||
|
MatSidenavModule,
|
||||||
|
MatCardModule,
|
||||||
|
OutsideClickModule,
|
||||||
|
MatProgressBarModule,
|
||||||
|
MatToolbarModule,
|
||||||
|
MatMenuModule,
|
||||||
|
MatSnackBarModule,
|
||||||
|
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
ThemeService,
|
||||||
|
{
|
||||||
|
provide: APP_INITIALIZER,
|
||||||
|
useFactory: appInitializerFn,
|
||||||
|
multi: true,
|
||||||
|
deps: [GrpcService],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: APP_INITIALIZER,
|
||||||
|
useFactory: stateHandlerFn,
|
||||||
|
multi: true,
|
||||||
|
deps: [StatehandlerService],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: AuthConfig,
|
||||||
|
useValue: authConfig,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: StatehandlerProcessorService,
|
||||||
|
useClass: StatehandlerProcessorServiceImpl,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: StatehandlerService,
|
||||||
|
useClass: StatehandlerServiceImpl,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: OAuthStorage,
|
||||||
|
useClass: StorageService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: GRPC_INTERCEPTORS,
|
||||||
|
multi: true,
|
||||||
|
useClass: GrpcAuthInterceptor,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: GRPC_INTERCEPTORS,
|
||||||
|
multi: true,
|
||||||
|
useClass: GrpcOrgInterceptor,
|
||||||
|
},
|
||||||
|
GrpcService,
|
||||||
|
AuthService,
|
||||||
|
AuthUserService,
|
||||||
|
],
|
||||||
|
bootstrap: [AppComponent],
|
||||||
|
})
|
||||||
|
export class AppModule { }
|
31
console/src/app/directives/has-role/has-role.directive.ts
Normal file
31
console/src/app/directives/has-role/has-role.directive.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
|
||||||
|
import { AuthUserService } from 'src/app/services/auth-user.service';
|
||||||
|
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: '[appHasRole]',
|
||||||
|
})
|
||||||
|
|
||||||
|
export class HasRoleDirective {
|
||||||
|
private hasView: boolean = false;
|
||||||
|
@Input() public set appHasRole(roles: string[]) {
|
||||||
|
if (roles && roles.length > 0) {
|
||||||
|
this.userService.isAllowed(roles).subscribe(isAllowed => {
|
||||||
|
if (isAllowed && !this.hasView) {
|
||||||
|
this.viewContainerRef.clear();
|
||||||
|
this.viewContainerRef.createEmbeddedView(this.templateRef);
|
||||||
|
} else if (this.hasView) {
|
||||||
|
console.log('User blocked!', roles, isAllowed);
|
||||||
|
this.viewContainerRef.clear();
|
||||||
|
this.hasView = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private userService: AuthUserService,
|
||||||
|
protected templateRef: TemplateRef<any>,
|
||||||
|
protected viewContainerRef: ViewContainerRef,
|
||||||
|
) { }
|
||||||
|
}
|
19
console/src/app/directives/has-role/has-role.module.ts
Normal file
19
console/src/app/directives/has-role/has-role.module.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
|
||||||
|
import { HasRoleDirective } from './has-role.directive';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
HasRoleDirective,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
HasRoleDirective,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class HasRoleModule { }
|
@ -0,0 +1,17 @@
|
|||||||
|
import { Directive, ElementRef, EventEmitter, HostListener, Output } from '@angular/core';
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: '[appOutsideClick]',
|
||||||
|
})
|
||||||
|
export class OutsideClickDirective {
|
||||||
|
constructor(private elementRef: ElementRef) { }
|
||||||
|
|
||||||
|
@Output() public clickOutside: EventEmitter<HTMLElement> = new EventEmitter();
|
||||||
|
|
||||||
|
@HostListener('document:click', ['$event.target']) onMouseEnter(targetElement: HTMLElement): void {
|
||||||
|
const clickedInside = this.elementRef.nativeElement.contains(targetElement);
|
||||||
|
if (!clickedInside) {
|
||||||
|
this.clickOutside.emit(targetElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
|
||||||
|
import { OutsideClickDirective } from './outside-click.directive';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
OutsideClickDirective,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
OutsideClickDirective,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class OutsideClickModule { }
|
@ -0,0 +1,32 @@
|
|||||||
|
import { Directive, ElementRef, EventEmitter, HostListener, Output } from '@angular/core';
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: '[appScrollable]',
|
||||||
|
})
|
||||||
|
export class ScrollableDirective {
|
||||||
|
// when using this directive, add overflow-y scroll to css
|
||||||
|
@Output() scrollPosition: EventEmitter<any> = new EventEmitter();
|
||||||
|
|
||||||
|
constructor(public el: ElementRef) { }
|
||||||
|
|
||||||
|
@HostListener('scroll', ['$event'])
|
||||||
|
public onScroll(event: any): void {
|
||||||
|
try {
|
||||||
|
const top = event.target.scrollTop;
|
||||||
|
const height = this.el.nativeElement.scrollHeight;
|
||||||
|
const offset = this.el.nativeElement.offsetHeight;
|
||||||
|
|
||||||
|
// emit bottom event
|
||||||
|
if (top > height - offset - 1) {
|
||||||
|
this.scrollPosition.emit('bottom');
|
||||||
|
}
|
||||||
|
|
||||||
|
// emit top event
|
||||||
|
if (top === 0) {
|
||||||
|
this.scrollPosition.emit('top');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
19
console/src/app/directives/scrollable/scrollable.module.ts
Normal file
19
console/src/app/directives/scrollable/scrollable.module.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
|
||||||
|
import { ScrollableDirective } from './scrollable.directive';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
ScrollableDirective,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
ScrollableDirective,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class ScrollableModule { }
|
24
console/src/app/guards/auth.guard.ts
Normal file
24
console/src/app/guards/auth.guard.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { AuthService } from '../services/auth.service';
|
||||||
|
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class AuthGuard implements CanActivate {
|
||||||
|
constructor(private auth: AuthService, private router: Router) { }
|
||||||
|
|
||||||
|
public canActivate(
|
||||||
|
_: ActivatedRouteSnapshot,
|
||||||
|
state: RouterStateSnapshot,
|
||||||
|
): Observable<boolean> | Promise<boolean> | boolean {
|
||||||
|
if (!this.auth.authenticated) {
|
||||||
|
return this.auth.authenticate();
|
||||||
|
}
|
||||||
|
return this.auth.authenticated;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
20
console/src/app/guards/role.guard.ts
Normal file
20
console/src/app/guards/role.guard.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { AuthUserService } from '../services/auth-user.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class RoleGuard implements CanActivate {
|
||||||
|
|
||||||
|
constructor(private userService: AuthUserService) { }
|
||||||
|
|
||||||
|
public canActivate(
|
||||||
|
route: ActivatedRouteSnapshot,
|
||||||
|
state: RouterStateSnapshot,
|
||||||
|
): Observable<boolean> {
|
||||||
|
return this.userService.isAllowed(route.data['roles']);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
<div class="card" appOutsideClick (clickOutside)="closeCard($event)">
|
||||||
|
<mat-icon class="avatar">account_circle</mat-icon>
|
||||||
|
<span class="u-name">{{profile?.firstName}} {{profile?.lastName}}</span>
|
||||||
|
<span class="u-email">{{profile?.userName}}</span>
|
||||||
|
<span class="iamuser" *ngIf="iamuser">IAM USER</span>
|
||||||
|
|
||||||
|
<button color="accent" (click)="editUserProfile()" mat-stroked-button>{{'USER.EDITACCOUNT' | translate}}</button>
|
||||||
|
<div class="l-accounts">
|
||||||
|
<mat-progress-bar *ngIf="loadingUsers" color="accent" mode="indeterminate"></mat-progress-bar>
|
||||||
|
<a class="row" *ngFor="let user of users" (click)="selectAccount(user.userName)">
|
||||||
|
<mat-icon class="small-avatar" svgIcon="mdi_account_circle_outline"></mat-icon>
|
||||||
|
<div class="col">
|
||||||
|
<span class="title">{{user.userName}}</span>
|
||||||
|
<span class="email">{{'USER.STATE.'+user.authState | translate}}</span>
|
||||||
|
</div>
|
||||||
|
<span class="fill-space"></span>
|
||||||
|
<mat-icon>keyboard_arrow_right</mat-icon>
|
||||||
|
</a>
|
||||||
|
<a class="row" (click)="selectAccount()">
|
||||||
|
<div class="icon-wrapper">
|
||||||
|
<mat-icon>add</mat-icon>
|
||||||
|
</div>
|
||||||
|
<span class="col">
|
||||||
|
<span class="title">{{'USER.ADDACCOUNT' | translate}}</span>
|
||||||
|
</span>
|
||||||
|
<span class="fill-space"></span>
|
||||||
|
<mat-icon>keyboard_arrow_right</mat-icon>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button color="accent" (click)="logout()" mat-stroked-button>logout everywhere</button>
|
||||||
|
</div>
|
@ -0,0 +1,112 @@
|
|||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: .5rem;
|
||||||
|
z-index: 200;
|
||||||
|
border: 1px solid #ffffff30;
|
||||||
|
width: 350px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem 0;
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
height: 80px;
|
||||||
|
width: 80px;
|
||||||
|
border-radius: 50%;
|
||||||
|
line-height: 80px;
|
||||||
|
font-size: 80px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.u-name {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.u-email {
|
||||||
|
font-size: .8rem;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iamuser {
|
||||||
|
font-size: 1rem;
|
||||||
|
background: -webkit-linear-gradient(rgb(240,140,53), rgb(233, 60, 231));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 1rem;
|
||||||
|
margin: .5rem;
|
||||||
|
|
||||||
|
.mat-button-wrapper {
|
||||||
|
font-size: .8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.l-accounts {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
border-top: 1px solid #ffffff30;
|
||||||
|
border-bottom: 1px solid #ffffff30;
|
||||||
|
padding: .5rem 0;
|
||||||
|
|
||||||
|
.row {
|
||||||
|
padding: .5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #ffffff10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-avatar {
|
||||||
|
height: 35px;
|
||||||
|
width: 35px;
|
||||||
|
line-height: 35px;
|
||||||
|
font-size: 35px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-wrapper {
|
||||||
|
height: 35px;
|
||||||
|
width: 35px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin: 0 1rem;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
mat-icon {
|
||||||
|
margin: auto;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.col {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: .9rem;
|
||||||
|
line-height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email {
|
||||||
|
color: #81868a;
|
||||||
|
font-size: .8rem;
|
||||||
|
line-height: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill-space {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AccountsCardComponent } from './accounts-card.component';
|
||||||
|
|
||||||
|
describe('AccountsCardComponent', () => {
|
||||||
|
let component: AccountsCardComponent;
|
||||||
|
let fixture: ComponentFixture<AccountsCardComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [AccountsCardComponent],
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(AccountsCardComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,66 @@
|
|||||||
|
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { AuthConfig } from 'angular-oauth2-oidc';
|
||||||
|
import { UserProfile, UserSessionView } from 'src/app/proto/generated/auth_pb';
|
||||||
|
import { AuthUserService } from 'src/app/services/auth-user.service';
|
||||||
|
import { AuthService } from 'src/app/services/auth.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-accounts-card',
|
||||||
|
templateUrl: './accounts-card.component.html',
|
||||||
|
styleUrls: ['./accounts-card.component.scss'],
|
||||||
|
})
|
||||||
|
export class AccountsCardComponent implements OnInit {
|
||||||
|
@Input() public profile!: UserProfile.AsObject;
|
||||||
|
@Input() public iamuser: boolean = false;
|
||||||
|
|
||||||
|
@Output() public close: EventEmitter<void> = new EventEmitter();
|
||||||
|
public users: UserSessionView.AsObject[] = [];
|
||||||
|
public loadingUsers: boolean = false;
|
||||||
|
constructor(public authService: AuthService, private router: Router, private userService: AuthUserService) { }
|
||||||
|
|
||||||
|
public ngOnInit(): void {
|
||||||
|
this.loadingUsers = true;
|
||||||
|
this.userService.getMyUserSessions().then(sessions => {
|
||||||
|
this.users = sessions.toObject().userSessionsList;
|
||||||
|
|
||||||
|
const index = this.users.findIndex(user => user.userName === this.profile.userName);
|
||||||
|
this.users.splice(index, 1);
|
||||||
|
|
||||||
|
this.loadingUsers = false;
|
||||||
|
}).catch(() => {
|
||||||
|
this.loadingUsers = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public editUserProfile(): void {
|
||||||
|
this.router.navigate(['user/me']);
|
||||||
|
this.close.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeCard(element: HTMLElement): void {
|
||||||
|
if (!element.classList.contains('dontcloseonclick')) {
|
||||||
|
this.close.emit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public selectAccount(loginHint?: string, idToken?: string): void {
|
||||||
|
const configWithPrompt: Partial<AuthConfig> = {
|
||||||
|
customQueryParams: {
|
||||||
|
prompt: 'select_account',
|
||||||
|
} as any,
|
||||||
|
};
|
||||||
|
if (loginHint) {
|
||||||
|
(configWithPrompt as any).customQueryParams['login_hint'] = loginHint;
|
||||||
|
}
|
||||||
|
if (idToken) {
|
||||||
|
(configWithPrompt as any).customQueryParams['id_token_hint'] = idToken;
|
||||||
|
}
|
||||||
|
this.authService.authenticate(configWithPrompt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public logout(): void {
|
||||||
|
this.router.navigate(['/']);
|
||||||
|
this.close.emit();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { OutsideClickModule } from 'src/app/directives/outside-click/outside-click.module';
|
||||||
|
|
||||||
|
import { AccountsCardComponent } from './accounts-card.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AccountsCardComponent,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatProgressBarModule,
|
||||||
|
OutsideClickModule,
|
||||||
|
TranslateModule,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
AccountsCardComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AccountsCardModule { }
|
@ -0,0 +1,33 @@
|
|||||||
|
<h1 mat-dialog-title>
|
||||||
|
<span class="title">{{'MEMBER.ADD' | translate}}</span>
|
||||||
|
</h1>
|
||||||
|
<p class="desc"> {{'ORG_DETAIL.MEMBER.ADDDESCRIPTION' | translate}}</p>
|
||||||
|
<div mat-dialog-content>
|
||||||
|
<app-search-user-autocomplete (selectionChanged)="users = $event"></app-search-user-autocomplete>
|
||||||
|
|
||||||
|
<mat-form-field class="full-width" appearance="outline"
|
||||||
|
*ngIf="creationType === CreationType.PROJECT_OWNED || creationType === CreationType.PROJECT_GRANTED">
|
||||||
|
<mat-label>{{ 'PROJECT.GRANT.TITLE' | translate }}</mat-label>
|
||||||
|
<mat-select [(ngModel)]="roles" multiple>
|
||||||
|
<mat-option *ngFor="let role of memberRoleOptions" [value]="role">
|
||||||
|
{{ 'ROLES.'+role | translate }}
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<ng-container *ngIf="creationType === CreationType.ORG">
|
||||||
|
<app-org-member-roles-autocomplete (selectionChanged)="setOrgMemberRoles($event)">
|
||||||
|
</app-org-member-roles-autocomplete>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div mat-dialog-actions class="action">
|
||||||
|
<button mat-button (click)="closeDialog()">
|
||||||
|
cancel
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button [disabled]="users.length == 0 || roles.length == 0" color="primary" mat-raised-button class="ok-button"
|
||||||
|
(click)="closeDialogWithSuccess()">
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
@ -0,0 +1,25 @@
|
|||||||
|
.title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc {
|
||||||
|
color: #81868a;
|
||||||
|
font-size: .9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-width {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
.ok-button {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ProjectMemberCreateDialogComponent } from './project-member-create-dialog.component';
|
||||||
|
|
||||||
|
|
||||||
|
describe('AddMemberDialogComponent', () => {
|
||||||
|
let component: ProjectMemberCreateDialogComponent;
|
||||||
|
let fixture: ComponentFixture<ProjectMemberCreateDialogComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ProjectMemberCreateDialogComponent],
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ProjectMemberCreateDialogComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,60 @@
|
|||||||
|
import { Component, Inject } from '@angular/core';
|
||||||
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
|
import { ProjectRole, User } from 'src/app/proto/generated/management_pb';
|
||||||
|
import { ProjectService } from 'src/app/services/project.service';
|
||||||
|
import { ToastService } from 'src/app/services/toast.service';
|
||||||
|
|
||||||
|
export enum CreationType {
|
||||||
|
PROJECT_OWNED = 0,
|
||||||
|
PROJECT_GRANTED = 1,
|
||||||
|
ORG = 2,
|
||||||
|
}
|
||||||
|
@Component({
|
||||||
|
selector: 'app-project-member-create-dialog',
|
||||||
|
templateUrl: './project-member-create-dialog.component.html',
|
||||||
|
styleUrls: ['./project-member-create-dialog.component.scss'],
|
||||||
|
})
|
||||||
|
export class ProjectMemberCreateDialogComponent {
|
||||||
|
public projectId: string = '';
|
||||||
|
public creationType!: CreationType;
|
||||||
|
public users: Array<User.AsObject> = [];
|
||||||
|
public roles: Array<ProjectRole.AsObject> | string[] = [];
|
||||||
|
public CreationType: any = CreationType;
|
||||||
|
public memberRoleOptions: string[] = [];
|
||||||
|
constructor(
|
||||||
|
private projectService: ProjectService,
|
||||||
|
public dialogRef: MatDialogRef<ProjectMemberCreateDialogComponent>,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||||
|
toastService: ToastService,
|
||||||
|
) {
|
||||||
|
this.creationType = data.creationType;
|
||||||
|
this.projectId = data.projectId;
|
||||||
|
|
||||||
|
if (this.creationType === CreationType.PROJECT_GRANTED) {
|
||||||
|
this.projectService.GetProjectGrantMemberRoles().then(resp => {
|
||||||
|
this.memberRoleOptions = resp.toObject().rolesList;
|
||||||
|
}).catch(error => {
|
||||||
|
toastService.showError(error.message);
|
||||||
|
});
|
||||||
|
} else if (this.creationType === CreationType.PROJECT_OWNED) {
|
||||||
|
this.projectService.GetProjectMemberRoles().then(resp => {
|
||||||
|
this.memberRoleOptions = resp.toObject().rolesList;
|
||||||
|
console.log(this.memberRoleOptions);
|
||||||
|
}).catch(error => {
|
||||||
|
toastService.showError(error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeDialog(): void {
|
||||||
|
this.dialogRef.close(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeDialogWithSuccess(): void {
|
||||||
|
this.dialogRef.close({ users: this.users, roles: this.roles });
|
||||||
|
}
|
||||||
|
|
||||||
|
public setOrgMemberRoles(roles: string[]): void {
|
||||||
|
this.roles = roles;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { SearchUserAutocompleteModule } from 'src/app/modules/search-user-autocomplete/search-user-autocomplete.module';
|
||||||
|
|
||||||
|
import {
|
||||||
|
OrgMemberRolesAutocompleteModule,
|
||||||
|
} from '../../pages/orgs/org-member-roles-autocomplete/org-member-roles-autocomplete.module';
|
||||||
|
import { SearchRolesAutocompleteModule } from '../search-roles-autocomplete/search-roles-autocomplete.module';
|
||||||
|
import { ProjectMemberCreateDialogComponent } from './project-member-create-dialog.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [ProjectMemberCreateDialogComponent],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
MatDialogModule,
|
||||||
|
MatButtonModule,
|
||||||
|
TranslateModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatSelectModule,
|
||||||
|
FormsModule,
|
||||||
|
SearchUserAutocompleteModule,
|
||||||
|
SearchRolesAutocompleteModule,
|
||||||
|
OrgMemberRolesAutocompleteModule,
|
||||||
|
],
|
||||||
|
entryComponents: [
|
||||||
|
ProjectMemberCreateDialogComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class ProjectMemberCreateDialogModule { }
|
17
console/src/app/modules/card/card.component.html
Normal file
17
console/src/app/modules/card/card.component.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<div class="card">
|
||||||
|
<div *ngIf="title || description" class="header" [ngClass]="{'bottom-margin': expanded}">
|
||||||
|
<div *ngIf="title" class="row">
|
||||||
|
<h2 class="title">{{title}}</h2>
|
||||||
|
<span class="fill-space"></span>
|
||||||
|
<ng-content select="card-actions"></ng-content>
|
||||||
|
<button class="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">{{description}}</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-content" *ngIf="expanded" [@openClose]="animate">
|
||||||
|
<ng-content></ng-content>
|
||||||
|
</div>
|
||||||
|
</div>
|
46
console/src/app/modules/card/card.component.scss
Normal file
46
console/src/app/modules/card/card.component.scss
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
|
||||||
|
.card {
|
||||||
|
margin: 1rem 0;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: .5rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
&.bottom-margin {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 400;
|
||||||
|
font-family: 'Rubik';
|
||||||
|
font-size: 1.2rem;
|
||||||
|
// margin-top: .3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill-space {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
margin-right: -.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc {
|
||||||
|
font-size: .9rem;
|
||||||
|
color: #81868a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
25
console/src/app/modules/card/card.component.spec.ts
Normal file
25
console/src/app/modules/card/card.component.spec.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { CardComponent } from './card.component';
|
||||||
|
|
||||||
|
describe('CardComponent', () => {
|
||||||
|
let component: CardComponent;
|
||||||
|
let fixture: ComponentFixture<CardComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [CardComponent],
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(CardComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
25
console/src/app/modules/card/card.component.ts
Normal file
25
console/src/app/modules/card/card.component.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { animate, style, transition, trigger } from '@angular/animations';
|
||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-card',
|
||||||
|
templateUrl: './card.component.html',
|
||||||
|
styleUrls: ['./card.component.scss'],
|
||||||
|
animations: [
|
||||||
|
trigger('openClose', [
|
||||||
|
transition(':enter', [
|
||||||
|
style({ height: '0', opacity: 0 }),
|
||||||
|
animate('150ms ease-in-out', style({ height: '*', opacity: 1 })),
|
||||||
|
]),
|
||||||
|
transition(':leave', [
|
||||||
|
animate('150ms ease-in-out', style({ height: '0', opacity: 0 })),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class CardComponent {
|
||||||
|
public expanded: boolean = true;
|
||||||
|
@Input() public title: string = '';
|
||||||
|
@Input() public description: string = '';
|
||||||
|
@Input() public animate: boolean = false;
|
||||||
|
}
|
23
console/src/app/modules/card/card.module.ts
Normal file
23
console/src/app/modules/card/card.module.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||||
|
|
||||||
|
import { CardComponent } from './card.component';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [CardComponent],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatTooltipModule,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
CardComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class CardModule { }
|
13
console/src/app/modules/changes/changes.component.html
Normal file
13
console/src/app/modules/changes/changes.component.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<span class="header">{{ 'CHANGES.LISTTITLE' | translate }}</span>
|
||||||
|
|
||||||
|
<div class="scroll-container" appScrollable (scrollPosition)="scrollHandler($event)">
|
||||||
|
<li class="item change-item-back" *ngFor="let event of data | async">
|
||||||
|
<span class="seq">
|
||||||
|
{{dateFromTimestamp(event.changeDate) | localizedDate: 'EEE dd. MMM, HH:mm'}}
|
||||||
|
</span>
|
||||||
|
<span class="desc">{{'CHANGES.EVENTS.'+event.eventType | translate}}</span>
|
||||||
|
</li>
|
||||||
|
<div class="sp-wrapper">
|
||||||
|
<mat-spinner *ngIf="loading | async" diameter="25"></mat-spinner>
|
||||||
|
</div>
|
||||||
|
</div>
|
40
console/src/app/modules/changes/changes.component.scss
Normal file
40
console/src/app/modules/changes/changes.component.scss
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
.header {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #81868a;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-container {
|
||||||
|
max-height: 540px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
|
||||||
|
.item {
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 60px;
|
||||||
|
padding: .5rem;
|
||||||
|
margin: .25rem 0;
|
||||||
|
border-radius: .5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.seq {
|
||||||
|
color: #81868a;
|
||||||
|
font-size: 12px;
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc {
|
||||||
|
overflow-x: auto;
|
||||||
|
font-size: .9rem;
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sp-wrapper {
|
||||||
|
padding: .5rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
25
console/src/app/modules/changes/changes.component.spec.ts
Normal file
25
console/src/app/modules/changes/changes.component.spec.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ChangesComponent } from './changes.component';
|
||||||
|
|
||||||
|
describe('ChangesComponent', () => {
|
||||||
|
let component: ChangesComponent;
|
||||||
|
let fixture: ComponentFixture<ChangesComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ChangesComponent],
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ChangesComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
124
console/src/app/modules/changes/changes.component.ts
Normal file
124
console/src/app/modules/changes/changes.component.ts
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
|
||||||
|
import { BehaviorSubject, from, Observable } from 'rxjs';
|
||||||
|
import { scan, take, tap } from 'rxjs/operators';
|
||||||
|
import { Change, Changes } from 'src/app/proto/generated/management_pb';
|
||||||
|
import { MgmtUserService } from 'src/app/services/mgmt-user.service';
|
||||||
|
|
||||||
|
export enum ChangeType {
|
||||||
|
USER = 'user',
|
||||||
|
ORG = 'org',
|
||||||
|
PROJECT = 'project',
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-changes',
|
||||||
|
templateUrl: './changes.component.html',
|
||||||
|
styleUrls: ['./changes.component.scss'],
|
||||||
|
})
|
||||||
|
export class ChangesComponent implements OnInit {
|
||||||
|
@Input() public changeType: ChangeType = ChangeType.USER;
|
||||||
|
@Input() public id: string = '';
|
||||||
|
|
||||||
|
// Source data
|
||||||
|
private _done: BehaviorSubject<any> = new BehaviorSubject(false);
|
||||||
|
private _loading: BehaviorSubject<any> = new BehaviorSubject(false);
|
||||||
|
private _data: BehaviorSubject<any> = new BehaviorSubject([]);
|
||||||
|
|
||||||
|
// Observable data
|
||||||
|
loading: Observable<boolean> = this._loading.asObservable();
|
||||||
|
public data!: Observable<Change.AsObject[]>;
|
||||||
|
public changes!: Changes.AsObject;
|
||||||
|
constructor(private mgmtUserService: MgmtUserService) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public scrollHandler(e: any): void {
|
||||||
|
if (e === 'bottom') {
|
||||||
|
this.more();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private init(): void {
|
||||||
|
let first: Promise<Changes>;
|
||||||
|
switch (this.changeType) {
|
||||||
|
case ChangeType.USER: first = this.mgmtUserService.UserChanges(this.id, 10, 0);
|
||||||
|
break;
|
||||||
|
case ChangeType.PROJECT: first = this.mgmtUserService.ProjectChanges(this.id, 20, 0);
|
||||||
|
break;
|
||||||
|
case ChangeType.ORG: first = this.mgmtUserService.OrgChanges(this.id, 10, 0);
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mapAndUpdate(first);
|
||||||
|
|
||||||
|
// Create the observable array for consumption in components
|
||||||
|
this.data = this._data.asObservable().pipe(
|
||||||
|
scan((acc, val) => {
|
||||||
|
return false ? val.concat(acc) : acc.concat(val);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private more(): void {
|
||||||
|
const cursor = this.getCursor();
|
||||||
|
|
||||||
|
let more: Promise<Changes>;
|
||||||
|
|
||||||
|
switch (this.changeType) {
|
||||||
|
case ChangeType.USER: more = this.mgmtUserService.UserChanges(this.id, 10, cursor);
|
||||||
|
break;
|
||||||
|
case ChangeType.PROJECT: more = this.mgmtUserService.ProjectChanges(this.id, 10, cursor);
|
||||||
|
break;
|
||||||
|
case ChangeType.ORG: more = this.mgmtUserService.OrgChanges(this.id, 10, cursor);
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mapAndUpdate(more);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determines the snapshot to paginate query
|
||||||
|
private getCursor(): number {
|
||||||
|
const current = this._data.value;
|
||||||
|
if (current.length) {
|
||||||
|
// return true ? current[0].sequence :
|
||||||
|
return current[current.length - 1].sequence;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maps the snapshot to usable format the updates source
|
||||||
|
private mapAndUpdate(col: Promise<Changes>): any {
|
||||||
|
if (this._done.value || this._loading.value) { return; }
|
||||||
|
|
||||||
|
// loading
|
||||||
|
this._loading.next(true);
|
||||||
|
|
||||||
|
// Map snapshot with doc ref (needed for cursor)
|
||||||
|
return from(col).pipe(
|
||||||
|
tap((res: Changes) => {
|
||||||
|
let values = res.toObject().changesList;
|
||||||
|
// If prepending, reverse the batch order
|
||||||
|
values = false ? values.reverse() : values;
|
||||||
|
|
||||||
|
// update source with new values, done loading
|
||||||
|
this._data.next(values);
|
||||||
|
// console.log(values);
|
||||||
|
this._loading.next(false);
|
||||||
|
|
||||||
|
// no more values, mark done
|
||||||
|
if (!values.length) {
|
||||||
|
this._done.next(true);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
take(1)).subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
public dateFromTimestamp(date: Timestamp.AsObject): any {
|
||||||
|
const ts: Date = new Date(date.seconds * 1000 + date.nanos / 1000);
|
||||||
|
return ts;
|
||||||
|
}
|
||||||
|
}
|
30
console/src/app/modules/changes/changes.module.ts
Normal file
30
console/src/app/modules/changes/changes.module.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { ScrollingModule } from '@angular/cdk/scrolling';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { ScrollableModule } from 'src/app/directives/scrollable/scrollable.module';
|
||||||
|
import { PipesModule } from 'src/app/pipes/pipes.module';
|
||||||
|
|
||||||
|
import { ChangesComponent } from './changes.component';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
ChangesComponent,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
ScrollableModule,
|
||||||
|
MatProgressSpinnerModule,
|
||||||
|
TranslateModule,
|
||||||
|
PipesModule,
|
||||||
|
ScrollingModule,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
ChangesComponent,
|
||||||
|
ScrollableModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class ChangesModule { }
|
@ -0,0 +1,8 @@
|
|||||||
|
<div class="meta-wrapper">
|
||||||
|
<div class="main-content">
|
||||||
|
<ng-content></ng-content>
|
||||||
|
</div>
|
||||||
|
<div class="meta">
|
||||||
|
<ng-content class="meta-content" select="metainfo"></ng-content>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,33 @@
|
|||||||
|
.meta-wrapper {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
display: relative;
|
||||||
|
width: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
flex: 1 0 300px;
|
||||||
|
@media only screen and (min-width: 1500px) {
|
||||||
|
flex-basis: 400px;
|
||||||
|
}
|
||||||
|
// overflow-y: auto;
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
.meta-content {
|
||||||
|
max-height: calc(100vh - 60px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media only screen and (max-width: 700px) {
|
||||||
|
flex-direction: column;
|
||||||
|
.main-content, .meta {
|
||||||
|
overflow-y: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { MetaLayoutComponent } from './meta-layout.component';
|
||||||
|
|
||||||
|
describe('MetaLayoutComponent', () => {
|
||||||
|
let component: MetaLayoutComponent;
|
||||||
|
let fixture: ComponentFixture<MetaLayoutComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [MetaLayoutComponent],
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(MetaLayoutComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,9 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-meta-layout',
|
||||||
|
templateUrl: './meta-layout.component.html',
|
||||||
|
styleUrls: ['./meta-layout.component.scss'],
|
||||||
|
})
|
||||||
|
export class MetaLayoutComponent {
|
||||||
|
}
|
15
console/src/app/modules/meta-layout/meta-layout.module.ts
Normal file
15
console/src/app/modules/meta-layout/meta-layout.module.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
|
||||||
|
import { MetaLayoutComponent } from './meta-layout.component';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [MetaLayoutComponent],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
],
|
||||||
|
exports: [MetaLayoutComponent],
|
||||||
|
})
|
||||||
|
export class MetaLayoutModule { }
|
@ -0,0 +1,56 @@
|
|||||||
|
import { DataSource } from '@angular/cdk/collections';
|
||||||
|
import { BehaviorSubject, from, Observable, of } from 'rxjs';
|
||||||
|
import { catchError, finalize, map } from 'rxjs/operators';
|
||||||
|
import { ProjectRole } from 'src/app/proto/generated/management_pb';
|
||||||
|
import { ProjectService } from 'src/app/services/project.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data source for the ProjectMembers view. This class should
|
||||||
|
* encapsulate all logic for fetching and manipulating the displayed data
|
||||||
|
* (including sorting, pagination, and filtering).
|
||||||
|
*/
|
||||||
|
export class ProjectRolesDataSource extends DataSource<ProjectRole.AsObject> {
|
||||||
|
public totalResult: number = 0;
|
||||||
|
public rolesSubject: BehaviorSubject<ProjectRole.AsObject[]> = new BehaviorSubject<ProjectRole.AsObject[]>([]);
|
||||||
|
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||||
|
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
|
||||||
|
|
||||||
|
constructor(private projectService: ProjectService) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public loadRoles(projectId: string, pageIndex: number, pageSize: number, sortDirection?: string): void {
|
||||||
|
const offset = pageIndex * pageSize;
|
||||||
|
|
||||||
|
this.loadingSubject.next(true);
|
||||||
|
from(this.projectService.SearchProjectRoles(projectId, pageSize, offset)).pipe(
|
||||||
|
map(resp => {
|
||||||
|
this.totalResult = resp.toObject().totalResult;
|
||||||
|
return resp.toObject().resultList;
|
||||||
|
}),
|
||||||
|
catchError(() => of([])),
|
||||||
|
finalize(() => this.loadingSubject.next(false)),
|
||||||
|
).subscribe(roles => {
|
||||||
|
this.rolesSubject.next(roles);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect this data source to the table. The table will only update when
|
||||||
|
* the returned stream emits new items.
|
||||||
|
* @returns A stream of the items to be rendered.
|
||||||
|
*/
|
||||||
|
public connect(): Observable<ProjectRole.AsObject[]> {
|
||||||
|
return this.rolesSubject.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the table is being destroyed. Use this function, to clean up
|
||||||
|
* any open connections or free any held resources that were set up during connect.
|
||||||
|
*/
|
||||||
|
public disconnect(): void {
|
||||||
|
this.rolesSubject.complete();
|
||||||
|
this.loadingSubject.complete();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,72 @@
|
|||||||
|
<div class="table-header-row" *ngIf="projectId">
|
||||||
|
<div class="col">
|
||||||
|
<ng-container *ngIf="!selection.hasValue()">
|
||||||
|
<span class="desc">{{'ORG_DETAIL.TABLE.TOTAL' | translate}}</span>
|
||||||
|
<span class="count">{{dataSource?.rolesSubject.value.length}}</span>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="selection.hasValue()">
|
||||||
|
<span class="desc">{{'ORG_DETAIL.TABLE.SELECTION' | translate}}</span>
|
||||||
|
<span class="count">{{selection?.selected?.length}}</span>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
<span class="fill-space"></span>
|
||||||
|
<ng-template appHasRole [appHasRole]="['project.role.delete']">
|
||||||
|
<button [disabled]="disabled" matTooltip="{{'PROJECT.ROLE.DELETE' | translate}}" class="icon-button"
|
||||||
|
(click)="deleteSelectedRoles()" mat-icon-button *ngIf="selection.hasValue() && actionsVisible">
|
||||||
|
<mat-icon>delete_outline</mat-icon>
|
||||||
|
</button>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template appHasRole [appHasRole]="['project.role.write:' + projectId, 'project.role.write']">
|
||||||
|
<a *ngIf="actionsVisible" [disabled]="disabled" class="add-button"
|
||||||
|
[routerLink]="[ '/projects', projectId, 'roles', 'create']" color="primary" mat-raised-button>
|
||||||
|
<mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }}
|
||||||
|
</a>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<div class="spinner-container" *ngIf="dataSource.loading$ | async">
|
||||||
|
<mat-spinner diameter="50"></mat-spinner>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table [dataSource]="dataSource" mat-table class="full-width-table" matSort aria-label="Elements">
|
||||||
|
<ng-container matColumnDef="select">
|
||||||
|
<th class="selection" mat-header-cell *matHeaderCellDef>
|
||||||
|
<mat-checkbox (change)="$event ? masterToggle() : null"
|
||||||
|
[checked]="selection.hasValue() && isAllSelected()"
|
||||||
|
[indeterminate]="selection.hasValue() && !isAllSelected()">
|
||||||
|
</mat-checkbox>
|
||||||
|
</th>
|
||||||
|
<td class="selection" mat-cell *matCellDef="let row">
|
||||||
|
<mat-checkbox (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.ROLE.NAME' | translate }} </th>
|
||||||
|
<td mat-cell *matCellDef="let role"> {{role.name}} </td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="displayname">
|
||||||
|
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.ROLE.DISPLAY_NAME' | translate }} </th>
|
||||||
|
<td mat-cell *matCellDef="let role"> {{role.displayName}} </td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="group">
|
||||||
|
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.ROLE.GROUP' | translate }} </th>
|
||||||
|
<td mat-cell *matCellDef="let role">
|
||||||
|
<span class="role app-label" *ngIf="role.group"
|
||||||
|
matTooltip="Add all of group {{role.group}} to selection"
|
||||||
|
(click)="selectAllOfGroup(role.group)">{{role.group}}</span>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||||
|
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<mat-paginator #paginator [length]="dataSource.totalResult" [pageSize]="50" [pageSizeOptions]="[25, 50, 100, 250]">
|
||||||
|
</mat-paginator>
|
||||||
|
</div>
|
@ -0,0 +1,84 @@
|
|||||||
|
|
||||||
|
.table-header-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.col {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
.desc {
|
||||||
|
font-size: .8rem;
|
||||||
|
color: #81868a;
|
||||||
|
}
|
||||||
|
.count {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill-space {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button {
|
||||||
|
margin-right: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-button {
|
||||||
|
border-radius: .5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrapper {
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
.spinner-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
table, mat-paginator {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #2d2e30;
|
||||||
|
|
||||||
|
td, th {
|
||||||
|
&:first-child {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-row {
|
||||||
|
&:hover {
|
||||||
|
background-color: #ffffff05;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection {
|
||||||
|
width: 50px;
|
||||||
|
max-width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-neg {
|
||||||
|
margin-left: -1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role {
|
||||||
|
display: inline-block;
|
||||||
|
margin: .25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pointer {
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||||
|
import { MatSortModule } from '@angular/material/sort';
|
||||||
|
import { MatTableModule } from '@angular/material/table';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
|
||||||
|
import { ProjectRolesComponent } from './project-roles.component';
|
||||||
|
|
||||||
|
describe('ProjectRolesComponent', () => {
|
||||||
|
let component: ProjectRolesComponent;
|
||||||
|
let fixture: ComponentFixture<ProjectRolesComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ProjectRolesComponent],
|
||||||
|
imports: [
|
||||||
|
NoopAnimationsModule,
|
||||||
|
MatPaginatorModule,
|
||||||
|
MatSortModule,
|
||||||
|
MatTableModule,
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ProjectRolesComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should compile', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
110
console/src/app/modules/project-roles/project-roles.component.ts
Normal file
110
console/src/app/modules/project-roles/project-roles.component.ts
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import { SelectionModel } from '@angular/cdk/collections';
|
||||||
|
import { AfterViewInit, Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
|
||||||
|
import { MatPaginator } from '@angular/material/paginator';
|
||||||
|
import { MatTable } from '@angular/material/table';
|
||||||
|
import { tap } from 'rxjs/operators';
|
||||||
|
import { ProjectRole } from 'src/app/proto/generated/management_pb';
|
||||||
|
import { ProjectService } from 'src/app/services/project.service';
|
||||||
|
import { ToastService } from 'src/app/services/toast.service';
|
||||||
|
|
||||||
|
import { ProjectRolesDataSource } from './project-roles-datasource';
|
||||||
|
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-project-roles',
|
||||||
|
templateUrl: './project-roles.component.html',
|
||||||
|
styleUrls: ['./project-roles.component.scss'],
|
||||||
|
})
|
||||||
|
export class ProjectRolesComponent implements AfterViewInit, OnInit {
|
||||||
|
@Input() public projectId: string = '';
|
||||||
|
@Input() public disabled: boolean = false;
|
||||||
|
@Input() public actionsVisible: boolean = false;
|
||||||
|
@ViewChild(MatPaginator) public paginator!: MatPaginator;
|
||||||
|
@ViewChild(MatTable) public table!: MatTable<ProjectRole.AsObject>;
|
||||||
|
public dataSource!: ProjectRolesDataSource;
|
||||||
|
public selection: SelectionModel<ProjectRole.AsObject> = new SelectionModel<ProjectRole.AsObject>(true, []);
|
||||||
|
@Output() public changedSelection: EventEmitter<Array<ProjectRole.AsObject>> = new EventEmitter();
|
||||||
|
|
||||||
|
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
|
||||||
|
public displayedColumns: string[] = ['select', 'name', 'displayname', 'group'];
|
||||||
|
|
||||||
|
constructor(private projectService: ProjectService, private toast: ToastService) { }
|
||||||
|
|
||||||
|
public ngOnInit(): void {
|
||||||
|
this.dataSource = new ProjectRolesDataSource(this.projectService);
|
||||||
|
this.dataSource.loadRoles(this.projectId, 0, 25, 'asc');
|
||||||
|
|
||||||
|
this.selection.changed.subscribe(() => {
|
||||||
|
this.changedSelection.emit(this.selection.selected);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngAfterViewInit(): void {
|
||||||
|
this.paginator.page
|
||||||
|
.pipe(
|
||||||
|
tap(() => this.loadRolesPage()),
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
public selectAllOfGroup(group: string): void {
|
||||||
|
const groupRoles: ProjectRole.AsObject[] = this.dataSource.rolesSubject.getValue()
|
||||||
|
.filter(role => role.group === group);
|
||||||
|
this.selection.select(...groupRoles);
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadRolesPage(): void {
|
||||||
|
this.dataSource.loadRoles(
|
||||||
|
this.projectId,
|
||||||
|
this.paginator.pageIndex,
|
||||||
|
this.paginator.pageSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isAllSelected(): boolean {
|
||||||
|
const numSelected = this.selection.selected.length;
|
||||||
|
const numRows = this.dataSource.rolesSubject.value.length;
|
||||||
|
return numSelected === numRows;
|
||||||
|
}
|
||||||
|
|
||||||
|
public masterToggle(): void {
|
||||||
|
this.isAllSelected() ?
|
||||||
|
this.selection.clear() :
|
||||||
|
this.dataSource.rolesSubject.value.forEach((row: ProjectRole.AsObject) => this.selection.select(row));
|
||||||
|
}
|
||||||
|
|
||||||
|
public deleteSelectedRoles(): Promise<any> {
|
||||||
|
const oldState = this.dataSource.rolesSubject.value;
|
||||||
|
const indexes = this.selection.selected.map(sel => {
|
||||||
|
return oldState.findIndex(iter => iter.key === sel.key);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(this.selection.selected.map(role => {
|
||||||
|
return this.projectService.RemoveProjectRole(role.projectId, role.key);
|
||||||
|
})).then(() => {
|
||||||
|
this.toast.showInfo('Deleted');
|
||||||
|
indexes.forEach(index => {
|
||||||
|
if (index > -1) {
|
||||||
|
oldState.splice(index, 1);
|
||||||
|
this.dataSource.rolesSubject.next(this.dataSource.rolesSubject.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.selection.clear();
|
||||||
|
}).catch(error => {
|
||||||
|
this.toast.showError(error?.message || 'Error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeRole(role: ProjectRole.AsObject, index: number): void {
|
||||||
|
this.projectService
|
||||||
|
.RemoveProjectRole(role.projectId, role.key)
|
||||||
|
.then(() => {
|
||||||
|
this.toast.showInfo('Role removed');
|
||||||
|
this.dataSource.rolesSubject.value.splice(index, 1);
|
||||||
|
this.dataSource.rolesSubject.next(this.dataSource.rolesSubject.value);
|
||||||
|
})
|
||||||
|
.catch(data => {
|
||||||
|
this.toast.showError(data.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
|
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||||
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
|
import { MatTableModule } from '@angular/material/table';
|
||||||
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
|
||||||
|
|
||||||
|
import { ProjectRolesComponent } from './project-roles.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [ProjectRolesComponent],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
MatButtonModule,
|
||||||
|
HasRoleModule,
|
||||||
|
MatTableModule,
|
||||||
|
MatPaginatorModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatProgressSpinnerModule,
|
||||||
|
MatCheckboxModule,
|
||||||
|
RouterModule,
|
||||||
|
MatTooltipModule,
|
||||||
|
TranslateModule,
|
||||||
|
MatMenuModule,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
ProjectRolesComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class ProjectRolesModule { }
|
@ -0,0 +1,28 @@
|
|||||||
|
<form>
|
||||||
|
<mat-form-field appearance="outline" class="full-width">
|
||||||
|
<mat-label>Project Name</mat-label>
|
||||||
|
<input matInput *ngIf="singleOutput" type="text" placeholder="Search for the project name" #nameInput
|
||||||
|
[formControl]="myControl" [matAutocomplete]="auto" />
|
||||||
|
|
||||||
|
<mat-chip-list *ngIf="!singleOutput" #chipList aria-label="name selection">
|
||||||
|
<mat-chip class="chip" *ngFor="let selectedProject of projects" [selectable]="selectable"
|
||||||
|
[removable]="removable" (removed)="remove(selectedProject)">
|
||||||
|
{{selectedProject.name}}
|
||||||
|
<mat-icon matChipRemove *ngIf="removable">cancel</mat-icon>
|
||||||
|
</mat-chip>
|
||||||
|
<input placeholder="{{'PROJECT.NAME' | translate}}" #nameInput [formControl]="myControl"
|
||||||
|
[matAutocomplete]="auto" [matChipInputFor]="chipList"
|
||||||
|
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" [matChipInputAddOnBlur]="addOnBlur"
|
||||||
|
(matChipInputTokenEnd)="add($event)" />
|
||||||
|
</mat-chip-list>
|
||||||
|
|
||||||
|
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)" [displayWith]="displayFn">
|
||||||
|
<mat-option *ngIf="isLoading" class="is-loading">
|
||||||
|
<mat-spinner diameter="30"></mat-spinner>
|
||||||
|
</mat-option>
|
||||||
|
<mat-option *ngFor="let project of filteredProjects" [value]="project">
|
||||||
|
{{project.name}}
|
||||||
|
</mat-option>
|
||||||
|
</mat-autocomplete>
|
||||||
|
</mat-form-field>
|
||||||
|
</form>
|
@ -0,0 +1,3 @@
|
|||||||
|
.full-width {
|
||||||
|
width: 100%;
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { SearchProjectAutocompleteComponent } from './search-project-autocomplete.component';
|
||||||
|
|
||||||
|
|
||||||
|
describe('SearchProjectComponent', () => {
|
||||||
|
let component: SearchProjectAutocompleteComponent;
|
||||||
|
let fixture: ComponentFixture<SearchProjectAutocompleteComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [SearchProjectAutocompleteComponent],
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(SearchProjectAutocompleteComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,108 @@
|
|||||||
|
import { COMMA, ENTER } from '@angular/cdk/keycodes';
|
||||||
|
import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core';
|
||||||
|
import { FormControl } from '@angular/forms';
|
||||||
|
import { MatAutocomplete, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
||||||
|
import { MatChipInputEvent } from '@angular/material/chips';
|
||||||
|
import { from } from 'rxjs';
|
||||||
|
import { debounceTime, switchMap, tap } from 'rxjs/operators';
|
||||||
|
import { Project, ProjectSearchKey, ProjectSearchQuery, SearchMethod } from 'src/app/proto/generated/management_pb';
|
||||||
|
import { ProjectService } from 'src/app/services/project.service';
|
||||||
|
import { ToastService } from 'src/app/services/toast.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-search-project-autocomplete',
|
||||||
|
templateUrl: './search-project-autocomplete.component.html',
|
||||||
|
styleUrls: ['./search-project-autocomplete.component.scss'],
|
||||||
|
})
|
||||||
|
export class SearchProjectAutocompleteComponent {
|
||||||
|
public selectable: boolean = true;
|
||||||
|
public removable: boolean = true;
|
||||||
|
public addOnBlur: boolean = true;
|
||||||
|
public separatorKeysCodes: number[] = [ENTER, COMMA];
|
||||||
|
public myControl: FormControl = new FormControl();
|
||||||
|
public names: string[] = [];
|
||||||
|
public projects: Array<Project.AsObject> = [];
|
||||||
|
public filteredProjects: Array<Project.AsObject> = [];
|
||||||
|
public isLoading: boolean = false;
|
||||||
|
@ViewChild('nameInput') public nameInput!: ElementRef<HTMLInputElement>;
|
||||||
|
@ViewChild('auto') public matAutocomplete!: MatAutocomplete;
|
||||||
|
@Input() public singleOutput: boolean = false;
|
||||||
|
@Output() public selectionChanged: EventEmitter<Project.AsObject[] | Project.AsObject> = new EventEmitter();
|
||||||
|
constructor(private projectService: ProjectService, private toast: ToastService) {
|
||||||
|
this.myControl.valueChanges
|
||||||
|
.pipe(
|
||||||
|
debounceTime(200),
|
||||||
|
tap(() => this.isLoading = true),
|
||||||
|
switchMap(value => {
|
||||||
|
const query = new ProjectSearchQuery();
|
||||||
|
query.setKey(ProjectSearchKey.PROJECTSEARCHKEY_PROJECT_NAME);
|
||||||
|
query.setValue(value);
|
||||||
|
query.setMethod(SearchMethod.SEARCHMETHOD_CONTAINS);
|
||||||
|
return from(this.projectService.SearchProjects(10, 0, [query]));
|
||||||
|
}),
|
||||||
|
// finalize(() => this.isLoading = false),
|
||||||
|
).subscribe((projects) => {
|
||||||
|
console.log(projects.toObject().resultList);
|
||||||
|
this.isLoading = false;
|
||||||
|
this.filteredProjects = projects.toObject().resultList;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public displayFn(project?: Project.AsObject): string | undefined {
|
||||||
|
return project ? `${project.name}` : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public add(event: MatChipInputEvent): void {
|
||||||
|
if (!this.matAutocomplete.isOpen) {
|
||||||
|
const input = event.input;
|
||||||
|
const value = event.value;
|
||||||
|
|
||||||
|
if ((value || '').trim()) {
|
||||||
|
const index = this.filteredProjects.findIndex((project) => {
|
||||||
|
if (project.name) {
|
||||||
|
return project.name === value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (index > -1) {
|
||||||
|
if (this.projects && this.projects.length > 0) {
|
||||||
|
this.projects.push(this.filteredProjects[index]);
|
||||||
|
} else {
|
||||||
|
this.projects = [this.filteredProjects[index]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input) {
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public remove(project: Project.AsObject): void {
|
||||||
|
const index = this.projects.indexOf(project);
|
||||||
|
|
||||||
|
if (index >= 0) {
|
||||||
|
this.projects.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public selected(event: MatAutocompleteSelectedEvent): void {
|
||||||
|
const index = this.filteredProjects.findIndex((project) => project === event.option.value);
|
||||||
|
if (index !== -1) {
|
||||||
|
if (this.singleOutput) {
|
||||||
|
this.selectionChanged.emit(this.filteredProjects[index]);
|
||||||
|
} else {
|
||||||
|
if (this.projects && this.projects.length > 0) {
|
||||||
|
this.projects.push(this.filteredProjects[index]);
|
||||||
|
} else {
|
||||||
|
this.projects = [this.filteredProjects[index]];
|
||||||
|
}
|
||||||
|
this.selectionChanged.emit(this.projects);
|
||||||
|
|
||||||
|
this.nameInput.nativeElement.value = '';
|
||||||
|
this.myControl.setValue(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatChipsModule } from '@angular/material/chips';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { SearchProjectAutocompleteComponent } from './search-project-autocomplete.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [SearchProjectAutocompleteComponent],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
MatAutocompleteModule,
|
||||||
|
MatChipsModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatInputModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
MatProgressSpinnerModule,
|
||||||
|
FormsModule,
|
||||||
|
TranslateModule,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
SearchProjectAutocompleteComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class SearchProjectAutocompleteModule { }
|
@ -0,0 +1,27 @@
|
|||||||
|
<form>
|
||||||
|
<mat-form-field appearance="outline" class="full-width">
|
||||||
|
<mat-label>Role Name</mat-label>
|
||||||
|
<input matInput *ngIf="singleOutput" type="text" placeholder="Search for the role name" #nameInput
|
||||||
|
[formControl]="myControl" [matAutocomplete]="auto" />
|
||||||
|
|
||||||
|
<mat-chip-list *ngIf="!singleOutput" #chipList aria-label="name selection">
|
||||||
|
<mat-chip class="chip" *ngFor="let selectedRole of roles" [selectable]="selectable" [removable]="removable"
|
||||||
|
(removed)="remove(selectedRole)">
|
||||||
|
{{selectedRole.displayName}}
|
||||||
|
<mat-icon matChipRemove *ngIf="removable">cancel</mat-icon>
|
||||||
|
</mat-chip>
|
||||||
|
<input placeholder="Role Name" #nameInput [formControl]="myControl" [matAutocomplete]="auto"
|
||||||
|
[matChipInputFor]="chipList" [matChipInputSeparatorKeyCodes]="separatorKeysCodes"
|
||||||
|
[matChipInputAddOnBlur]="addOnBlur" (matChipInputTokenEnd)="add($event)" />
|
||||||
|
</mat-chip-list>
|
||||||
|
|
||||||
|
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)" [displayWith]="displayFn">
|
||||||
|
<mat-option *ngIf="isLoading" class="is-loading">
|
||||||
|
<mat-spinner diameter="30"></mat-spinner>
|
||||||
|
</mat-option>
|
||||||
|
<mat-option *ngFor="let role of filteredRoles" [value]="role.key">
|
||||||
|
{{role.displayName}}
|
||||||
|
</mat-option>
|
||||||
|
</mat-autocomplete>
|
||||||
|
</mat-form-field>
|
||||||
|
</form>
|
@ -0,0 +1,3 @@
|
|||||||
|
.full-width {
|
||||||
|
width: 100%;
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { SearchRolesAutocompleteComponent } from './search-roles-autocomplete.component';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
describe('SearchProjectComponent', () => {
|
||||||
|
let component: SearchRolesAutocompleteComponent;
|
||||||
|
let fixture: ComponentFixture<SearchRolesAutocompleteComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [SearchRolesAutocompleteComponent],
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(SearchRolesAutocompleteComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,117 @@
|
|||||||
|
import { COMMA, ENTER } from '@angular/cdk/keycodes';
|
||||||
|
import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core';
|
||||||
|
import { FormControl } from '@angular/forms';
|
||||||
|
import { MatAutocomplete, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
||||||
|
import { MatChipInputEvent } from '@angular/material/chips';
|
||||||
|
import { from } from 'rxjs';
|
||||||
|
import { debounceTime, switchMap, tap } from 'rxjs/operators';
|
||||||
|
import {
|
||||||
|
ProjectRole,
|
||||||
|
ProjectRoleSearchKey,
|
||||||
|
ProjectRoleSearchQuery,
|
||||||
|
SearchMethod,
|
||||||
|
} from 'src/app/proto/generated/management_pb';
|
||||||
|
import { ProjectService } from 'src/app/services/project.service';
|
||||||
|
import { ToastService } from 'src/app/services/toast.service';
|
||||||
|
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-search-roles-autocomplete',
|
||||||
|
templateUrl: './search-roles-autocomplete.component.html',
|
||||||
|
styleUrls: ['./search-roles-autocomplete.component.scss'],
|
||||||
|
})
|
||||||
|
export class SearchRolesAutocompleteComponent {
|
||||||
|
public selectable: boolean = true;
|
||||||
|
public removable: boolean = true;
|
||||||
|
public addOnBlur: boolean = true;
|
||||||
|
public separatorKeysCodes: number[] = [ENTER, COMMA];
|
||||||
|
public myControl: FormControl = new FormControl();
|
||||||
|
public names: string[] = [];
|
||||||
|
public roles: Array<ProjectRole.AsObject> = [];
|
||||||
|
public filteredRoles: Array<ProjectRole.AsObject> = [];
|
||||||
|
public isLoading: boolean = false;
|
||||||
|
@ViewChild('nameInput') public nameInput!: ElementRef<HTMLInputElement>;
|
||||||
|
@ViewChild('auto') public matAutocomplete!: MatAutocomplete;
|
||||||
|
@Input() public projectId: string = '';
|
||||||
|
@Input() public singleOutput: boolean = false;
|
||||||
|
@Output() public selectionChanged: EventEmitter<ProjectRole.AsObject[] | ProjectRole.AsObject> = new EventEmitter();
|
||||||
|
constructor(private projectService: ProjectService, private toast: ToastService) {
|
||||||
|
this.myControl.valueChanges
|
||||||
|
.pipe(
|
||||||
|
debounceTime(200),
|
||||||
|
tap(() => this.isLoading = true),
|
||||||
|
switchMap(value => {
|
||||||
|
const query = new ProjectRoleSearchQuery();
|
||||||
|
query.setKey(ProjectRoleSearchKey.PROJECTROLESEARCHKEY_DISPLAY_NAME);
|
||||||
|
query.setMethod(SearchMethod.SEARCHMETHOD_CONTAINS);
|
||||||
|
query.setValue(value);
|
||||||
|
return from(this.projectService.SearchProjectRoles(this.projectId, 10, 0, [query]));
|
||||||
|
}),
|
||||||
|
).subscribe((roles) => {
|
||||||
|
console.log(roles.toObject().resultList);
|
||||||
|
this.isLoading = false;
|
||||||
|
this.filteredRoles = roles.toObject().resultList;
|
||||||
|
}, error => {
|
||||||
|
console.log(error);
|
||||||
|
this.isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public displayFn(project?: ProjectRole.AsObject): string | undefined {
|
||||||
|
return project ? `${project.displayName}` : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public add(event: MatChipInputEvent): void {
|
||||||
|
if (!this.matAutocomplete.isOpen) {
|
||||||
|
const input = event.input;
|
||||||
|
const value = event.value;
|
||||||
|
|
||||||
|
if ((value || '').trim()) {
|
||||||
|
const index = this.filteredRoles.findIndex((role) => {
|
||||||
|
if (role.key) {
|
||||||
|
return role.key === value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (index > -1) {
|
||||||
|
if (this.roles && this.roles.length > 0) {
|
||||||
|
this.roles.push(this.filteredRoles[index]);
|
||||||
|
} else {
|
||||||
|
this.roles = [this.filteredRoles[index]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input) {
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public remove(role: ProjectRole.AsObject): void {
|
||||||
|
const index = this.roles.indexOf(role);
|
||||||
|
|
||||||
|
if (index >= 0) {
|
||||||
|
this.roles.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public selected(event: MatAutocompleteSelectedEvent): void {
|
||||||
|
const index = this.filteredRoles.findIndex((role) => role.key === event.option.value);
|
||||||
|
if (index !== -1) {
|
||||||
|
if (this.singleOutput) {
|
||||||
|
this.selectionChanged.emit(this.filteredRoles[index]);
|
||||||
|
} else {
|
||||||
|
if (this.roles && this.roles.length > 0) {
|
||||||
|
this.roles.push(this.filteredRoles[index]);
|
||||||
|
} else {
|
||||||
|
this.roles = [this.filteredRoles[index]];
|
||||||
|
}
|
||||||
|
this.selectionChanged.emit(this.roles);
|
||||||
|
|
||||||
|
this.nameInput.nativeElement.value = '';
|
||||||
|
this.myControl.setValue(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatChipsModule } from '@angular/material/chips';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { SearchRolesAutocompleteComponent } from './search-roles-autocomplete.component';
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [SearchRolesAutocompleteComponent],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
MatAutocompleteModule,
|
||||||
|
MatChipsModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatInputModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
MatProgressSpinnerModule,
|
||||||
|
FormsModule,
|
||||||
|
TranslateModule,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
SearchRolesAutocompleteComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class SearchRolesAutocompleteModule { }
|
@ -0,0 +1,54 @@
|
|||||||
|
<form>
|
||||||
|
<mat-form-field *ngIf="target == UserTarget.SELF" appearance="outline" class="full-width">
|
||||||
|
<mat-label>Organizations User Email</mat-label>
|
||||||
|
|
||||||
|
<input matInput *ngIf="singleOutput" type="text" placeholder="Search for the user email" #usernameInput
|
||||||
|
[formControl]="myControl" [matAutocomplete]="auto" />
|
||||||
|
|
||||||
|
<mat-chip-list *ngIf="!singleOutput" #chipList aria-label="useremail selection">
|
||||||
|
<mat-chip class="chip" *ngFor="let selecteduser of users" [selectable]="selectable" [removable]="removable"
|
||||||
|
(removed)="remove(selecteduser)">
|
||||||
|
{{ selecteduser?.firstName }} {{selecteduser.lastName}} | <small> {{selecteduser.email}}</small>
|
||||||
|
<mat-icon matChipRemove *ngIf="removable">cancel</mat-icon>
|
||||||
|
</mat-chip>
|
||||||
|
<input placeholder="{{'ORG_DETAIL.MEMBER.EMAIL' | translate}}" #usernameInput [formControl]="myControl"
|
||||||
|
[matAutocomplete]="auto" [matChipInputFor]="chipList"
|
||||||
|
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" [matChipInputAddOnBlur]="addOnBlur"
|
||||||
|
(matChipInputTokenEnd)="add($event)" />
|
||||||
|
</mat-chip-list>
|
||||||
|
|
||||||
|
<!-- <mat-hint *ngIf="hint">
|
||||||
|
{{hint}}
|
||||||
|
<a (click)=" changeTarget()">{{'USER.TARGET.CLICKHERE' | translate}}</a>
|
||||||
|
</mat-hint> -->
|
||||||
|
|
||||||
|
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)" [displayWith]="displayFn">
|
||||||
|
<mat-option *ngIf="isLoading" class="is-loading">
|
||||||
|
<mat-spinner diameter="30"></mat-spinner>
|
||||||
|
</mat-option>
|
||||||
|
<mat-option *ngFor="let user of filteredUsers" [value]="user">
|
||||||
|
{{user.firstName}} {{user.lastName}}
|
||||||
|
<small>{{user.email}}</small>
|
||||||
|
</mat-option>
|
||||||
|
</mat-autocomplete>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<div *ngIf="target == UserTarget.EXTERNAL" class="line">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>Global User Email</mat-label>
|
||||||
|
<input matInput type="text" [formControl]="globalEmailControl" />
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<button mat-icon-button (click)="getGlobalUser()">
|
||||||
|
<mat-icon>search</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p *ngIf="target == UserTarget.EXTERNAL && users.length > 0">{{'USER.SEARCH.FOUND' | translate}}: <span
|
||||||
|
*ngFor="let user of users; index as i">{{user.email}} <mat-icon class="sm-dlt" (click)="users.splice(i, 1)">
|
||||||
|
remove_circle</mat-icon></span></p>
|
||||||
|
|
||||||
|
<p class="target-desc">{{(target == UserTarget.SELF ? 'USER.TARGET.SELF' : 'USER.TARGET.EXTERNAL') | translate}}
|
||||||
|
<a (click)="changeTarget()">{{'USER.TARGET.CLICKHERE' | translate}}</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
@ -0,0 +1,36 @@
|
|||||||
|
.full-width {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.target-desc {
|
||||||
|
color: #81868a;
|
||||||
|
font-size: .8rem;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
a {
|
||||||
|
color: white;
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color: white;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.line {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
mat-form-field {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sm-dlt {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: .8rem;
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { SearchUserAutocompleteComponent } from './search-user-autocomplete.component';
|
||||||
|
|
||||||
|
describe('SearchUserAutocompleteComponent', () => {
|
||||||
|
let component: SearchUserAutocompleteComponent;
|
||||||
|
let fixture: ComponentFixture<SearchUserAutocompleteComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [SearchUserAutocompleteComponent],
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(SearchUserAutocompleteComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,146 @@
|
|||||||
|
import { COMMA, ENTER } from '@angular/cdk/keycodes';
|
||||||
|
import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core';
|
||||||
|
import { FormControl } from '@angular/forms';
|
||||||
|
import { MatAutocomplete, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
||||||
|
import { MatChipInputEvent } from '@angular/material/chips';
|
||||||
|
import { from, of, Subject } from 'rxjs';
|
||||||
|
import { debounceTime, switchMap, takeUntil, tap } from 'rxjs/operators';
|
||||||
|
import { SearchMethod, User, UserSearchKey, UserSearchQuery } from 'src/app/proto/generated/management_pb';
|
||||||
|
import { MgmtUserService } from 'src/app/services/mgmt-user.service';
|
||||||
|
import { ToastService } from 'src/app/services/toast.service';
|
||||||
|
|
||||||
|
export enum UserTarget {
|
||||||
|
SELF = 'self',
|
||||||
|
EXTERNAL = 'external',
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-search-user-autocomplete',
|
||||||
|
templateUrl: './search-user-autocomplete.component.html',
|
||||||
|
styleUrls: ['./search-user-autocomplete.component.scss'],
|
||||||
|
})
|
||||||
|
export class SearchUserAutocompleteComponent {
|
||||||
|
public selectable: boolean = true;
|
||||||
|
public removable: boolean = true;
|
||||||
|
public addOnBlur: boolean = true;
|
||||||
|
public separatorKeysCodes: number[] = [ENTER, COMMA];
|
||||||
|
|
||||||
|
public myControl: FormControl = new FormControl();
|
||||||
|
public globalEmailControl: FormControl = new FormControl();
|
||||||
|
|
||||||
|
public emails: string[] = [];
|
||||||
|
public users: Array<User.AsObject> = [];
|
||||||
|
public filteredUsers: Array<User.AsObject> = [];
|
||||||
|
public isLoading: boolean = false;
|
||||||
|
public target: UserTarget = UserTarget.SELF;
|
||||||
|
public hint: string = '';
|
||||||
|
public UserTarget: any = UserTarget;
|
||||||
|
@ViewChild('usernameInput') public usernameInput!: ElementRef<HTMLInputElement>;
|
||||||
|
@ViewChild('auto') public matAutocomplete!: MatAutocomplete;
|
||||||
|
@Output() public selectionChanged: EventEmitter<User.AsObject | User.AsObject[]> = new EventEmitter();
|
||||||
|
@Input() public singleOutput: boolean = false;
|
||||||
|
private unsubscribed$: Subject<void> = new Subject();
|
||||||
|
constructor(private userService: MgmtUserService, private toast: ToastService) {
|
||||||
|
this.getFilteredResults();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFilteredResults(): void {
|
||||||
|
this.myControl.valueChanges.pipe(debounceTime(200),
|
||||||
|
takeUntil(this.unsubscribed$),
|
||||||
|
tap(() => this.isLoading = true),
|
||||||
|
switchMap(value => {
|
||||||
|
const query = new UserSearchQuery();
|
||||||
|
query.setKey(UserSearchKey.USERSEARCHKEY_EMAIL);
|
||||||
|
query.setValue(value);
|
||||||
|
query.setMethod(SearchMethod.SEARCHMETHOD_CONTAINS);
|
||||||
|
if (this.target === UserTarget.SELF) {
|
||||||
|
return from(this.userService.SearchUsers(10, 0, [query]));
|
||||||
|
} else {
|
||||||
|
return of(); // from(this.userService.GetUserByEmailGlobal(value));
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
).subscribe((userresp: any) => {
|
||||||
|
this.isLoading = false;
|
||||||
|
if (this.target === UserTarget.SELF && userresp) {
|
||||||
|
this.filteredUsers = userresp.toObject().resultList;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public displayFn(user?: User.AsObject): string | undefined {
|
||||||
|
return user ? `${user.email}` : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public add(event: MatChipInputEvent): void {
|
||||||
|
if (!this.matAutocomplete.isOpen) {
|
||||||
|
const input = event.input;
|
||||||
|
const value = event.value;
|
||||||
|
|
||||||
|
if ((value || '').trim()) {
|
||||||
|
const index = this.filteredUsers.findIndex((user) => {
|
||||||
|
if (user.email) {
|
||||||
|
return user.email === value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (index > -1) {
|
||||||
|
if (this.users && this.users.length > 0) {
|
||||||
|
this.users.push(this.filteredUsers[index]);
|
||||||
|
} else {
|
||||||
|
this.users = [this.filteredUsers[index]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input) {
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public remove(user: User.AsObject): void {
|
||||||
|
const index = this.users.indexOf(user);
|
||||||
|
|
||||||
|
if (index >= 0) {
|
||||||
|
this.users.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public selected(event: MatAutocompleteSelectedEvent): void {
|
||||||
|
const index = this.filteredUsers.findIndex((user) => user === event.option.value);
|
||||||
|
if (index !== -1) {
|
||||||
|
if (this.singleOutput) {
|
||||||
|
this.selectionChanged.emit(this.filteredUsers[index]);
|
||||||
|
} else {
|
||||||
|
if (this.users && this.users.length > 0) {
|
||||||
|
this.users.push(this.filteredUsers[index]);
|
||||||
|
} else {
|
||||||
|
this.users = [this.filteredUsers[index]];
|
||||||
|
}
|
||||||
|
this.selectionChanged.emit(this.users);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.usernameInput.nativeElement.value = '';
|
||||||
|
this.myControl.setValue(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public changeTarget(): void {
|
||||||
|
if (this.target === UserTarget.SELF) {
|
||||||
|
this.target = UserTarget.EXTERNAL;
|
||||||
|
this.filteredUsers = [];
|
||||||
|
this.unsubscribed$.next(); // clear old subscription
|
||||||
|
} else if (this.target === UserTarget.EXTERNAL) {
|
||||||
|
this.target = UserTarget.SELF;
|
||||||
|
this.getFilteredResults(); // new subscription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getGlobalUser(): void {
|
||||||
|
this.userService.GetUserByEmailGlobal(this.globalEmailControl.value).then(user => {
|
||||||
|
this.users = [user.toObject()];
|
||||||
|
this.selectionChanged.emit(this.users);
|
||||||
|
}).catch(error => {
|
||||||
|
this.toast.showError(error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatChipsModule } from '@angular/material/chips';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { SearchUserAutocompleteComponent } from './search-user-autocomplete.component';
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [SearchUserAutocompleteComponent],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
MatAutocompleteModule,
|
||||||
|
MatChipsModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatIconModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
MatProgressSpinnerModule,
|
||||||
|
FormsModule,
|
||||||
|
TranslateModule,
|
||||||
|
],
|
||||||
|
exports: [SearchUserAutocompleteComponent],
|
||||||
|
})
|
||||||
|
export class SearchUserAutocompleteModule { }
|
@ -0,0 +1,86 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="abort-container">
|
||||||
|
<button (click)="close()" mat-icon-button>
|
||||||
|
<mat-icon>close</mat-icon>
|
||||||
|
</button>
|
||||||
|
<span class="abort">{{ 'APP.PAGES.CREATE_OIDC' | translate }}</span><span class="abort-2">Step
|
||||||
|
{{ currentCreateStep }} of
|
||||||
|
{{ createSteps }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1>{{'APP.PAGES.CREATE_OIDC_DESc' | translate}}</h1>
|
||||||
|
<form [formGroup]="form" (ngSubmit)="saveOIDCApp()">
|
||||||
|
<div class="content">
|
||||||
|
<mat-form-field appearance="outline" class="formfield">
|
||||||
|
<mat-label>{{ 'APP.NAME' | translate }}</mat-label>
|
||||||
|
<input matInput formControlName="name" />
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>{{ 'APP.OIDC.RESPONSE' | translate }}</mat-label>
|
||||||
|
<mat-select formControlName="responseTypesList" multiple>
|
||||||
|
<mat-option *ngFor="let type of oidcResponseTypes" [value]="type">
|
||||||
|
{{ 'APP.OIDC.RESPONSE'+type | translate }}
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>{{ 'APP.OIDC.GRANT' | translate }}</mat-label>
|
||||||
|
<mat-select formControlName="grantTypesList" multiple>
|
||||||
|
<mat-option *ngFor="let grant of oidcGrantTypes" [value]="grant">
|
||||||
|
{{ 'APP.OIDC.GRANT'+grant | translate }}
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="formfield">
|
||||||
|
<mat-label>{{ 'APP.OIDC.APPTYPE' | translate }}</mat-label>
|
||||||
|
<mat-select formControlName="applicationType">
|
||||||
|
<mat-option *ngFor="let type of oidcAppTypes" [value]="type">
|
||||||
|
{{ 'APP.OIDC.APPTYPE'+type | translate }}
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="formfield">
|
||||||
|
<mat-label>{{ 'APP.OIDC.AUTHMETHOD' | translate }}</mat-label>
|
||||||
|
<mat-select formControlName="authMethodType">
|
||||||
|
<mat-option *ngFor="let type of oidcAuthMethodType" [value]="type">
|
||||||
|
{{ 'APP.OIDC.AUTHMETHOD'+type | translate }}
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="full-width">
|
||||||
|
<mat-label>{{ 'APP.OIDC.REDIRECT' | translate }}</mat-label>
|
||||||
|
<mat-chip-list #chipRedirectList aria-label="uri selection">
|
||||||
|
<mat-chip *ngFor="let uri of oidcApp.redirectUrisList" removable
|
||||||
|
(removed)="removeUri(uri, 'REDIRECT')">
|
||||||
|
{{uri}} <mat-icon matChipRemove>cancel</mat-icon>
|
||||||
|
</mat-chip>
|
||||||
|
<input [matChipInputFor]="chipRedirectList" [matChipInputSeparatorKeyCodes]="separatorKeysCodes"
|
||||||
|
[matChipInputAddOnBlur]="addOnBlur" (matChipInputTokenEnd)="addUri($event, 'REDIRECT')">
|
||||||
|
</mat-chip-list>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="full-width">
|
||||||
|
<mat-label>{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}</mat-label>
|
||||||
|
<mat-chip-list #chipPostRedirectList aria-label="uri selection">
|
||||||
|
<mat-chip *ngFor="let uri of oidcApp.postLogoutRedirectUrisList" removable
|
||||||
|
(removed)="removeUri(uri, 'POSTREDIRECT')">
|
||||||
|
{{uri}} <mat-icon matChipRemove>cancel</mat-icon>
|
||||||
|
</mat-chip>
|
||||||
|
<input [matChipInputFor]="chipPostRedirectList" [matChipInputSeparatorKeyCodes]="separatorKeysCodes"
|
||||||
|
[matChipInputAddOnBlur]="addOnBlur" (matChipInputTokenEnd)="addUri($event, 'POSTREDIRECT')">
|
||||||
|
</mat-chip-list>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button color="primary" mat-raised-button class="continue-button" [disabled]="form.invalid" cdkFocusInitial
|
||||||
|
type="submit">
|
||||||
|
{{ 'ACTIONS.SAVE' | translate }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
@ -0,0 +1,75 @@
|
|||||||
|
h1 {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 4rem 4rem 2rem 4rem;
|
||||||
|
|
||||||
|
.abort-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
|
||||||
|
.abort {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin-left: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.abort-2 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin-left: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-right {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.formfield {
|
||||||
|
width: 400px;
|
||||||
|
input {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.autocomplete {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
margin-top: 2rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.access-token {
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
margin: 0 -.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
mat-form-field {
|
||||||
|
flex: 1 0 40%;
|
||||||
|
margin: 0 .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-width {
|
||||||
|
flex-basis: 80%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.continue-button {
|
||||||
|
margin-top: 3rem;
|
||||||
|
display: block;
|
||||||
|
padding: 0.5rem 4rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AppCreateComponent } from './app-create.component';
|
||||||
|
|
||||||
|
describe('AppCreateComponent', () => {
|
||||||
|
let component: AppCreateComponent;
|
||||||
|
let fixture: ComponentFixture<AppCreateComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [AppCreateComponent],
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(AppCreateComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
186
console/src/app/pages/apps/app-create/app-create.component.ts
Normal file
186
console/src/app/pages/apps/app-create/app-create.component.ts
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes';
|
||||||
|
import { Location } from '@angular/common';
|
||||||
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||||
|
import { MatChipInputEvent } from '@angular/material/chips';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { ActivatedRoute, Params, Router } from '@angular/router';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
import {
|
||||||
|
Application,
|
||||||
|
OIDCApplicationCreate,
|
||||||
|
OIDCApplicationType,
|
||||||
|
OIDCAuthMethodType,
|
||||||
|
OIDCGrantType,
|
||||||
|
OIDCResponseType,
|
||||||
|
} from 'src/app/proto/generated/management_pb';
|
||||||
|
import { ProjectService } from 'src/app/services/project.service';
|
||||||
|
import { ToastService } from 'src/app/services/toast.service';
|
||||||
|
|
||||||
|
import { AppSecretDialogComponent } from '../app-secret-dialog/app-secret-dialog.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-app-create',
|
||||||
|
templateUrl: './app-create.component.html',
|
||||||
|
styleUrls: ['./app-create.component.scss'],
|
||||||
|
})
|
||||||
|
export class AppCreateComponent implements OnInit, OnDestroy {
|
||||||
|
private subscription?: Subscription;
|
||||||
|
public projectId: string = '';
|
||||||
|
public oidcApp: OIDCApplicationCreate.AsObject = new OIDCApplicationCreate().toObject();
|
||||||
|
public oidcResponseTypes: OIDCResponseType[] = [
|
||||||
|
OIDCResponseType.OIDCRESPONSETYPE_CODE,
|
||||||
|
OIDCResponseType.OIDCRESPONSETYPE_ID_TOKEN,
|
||||||
|
OIDCResponseType.OIDCRESPONSETYPE_TOKEN,
|
||||||
|
];
|
||||||
|
public oidcGrantTypes: OIDCGrantType[] = [
|
||||||
|
OIDCGrantType.OIDCGRANTTYPE_AUTHORIZATION_CODE,
|
||||||
|
OIDCGrantType.OIDCGRANTTYPE_IMPLICIT,
|
||||||
|
OIDCGrantType.OIDCGRANTTYPE_REFRESH_TOKEN,
|
||||||
|
];
|
||||||
|
public oidcAppTypes: OIDCApplicationType[] = [
|
||||||
|
OIDCApplicationType.OIDCAPPLICATIONTYPE_WEB,
|
||||||
|
OIDCApplicationType.OIDCAPPLICATIONTYPE_USER_AGENT,
|
||||||
|
OIDCApplicationType.OIDCAPPLICATIONTYPE_NATIVE,
|
||||||
|
];
|
||||||
|
public oidcAuthMethodType: OIDCAuthMethodType[] = [
|
||||||
|
OIDCAuthMethodType.OIDCAUTHMETHODTYPE_BASIC,
|
||||||
|
OIDCAuthMethodType.OIDCAUTHMETHODTYPE_NONE,
|
||||||
|
OIDCAuthMethodType.OIDCAUTHMETHODTYPE_POST,
|
||||||
|
];
|
||||||
|
|
||||||
|
public form!: FormGroup;
|
||||||
|
public createSteps: number = 1;
|
||||||
|
public currentCreateStep: number = 1;
|
||||||
|
public postLogoutRedirectUrisList: string[] = [];
|
||||||
|
|
||||||
|
public addOnBlur: boolean = true;
|
||||||
|
public readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private router: Router,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private toast: ToastService,
|
||||||
|
private dialog: MatDialog,
|
||||||
|
private projectService: ProjectService,
|
||||||
|
private fb: FormBuilder,
|
||||||
|
private _location: Location,
|
||||||
|
) {
|
||||||
|
this.form = this.fb.group({
|
||||||
|
name: ['', [Validators.required]],
|
||||||
|
responseTypesList: ['', []],
|
||||||
|
grantTypesList: ['', []],
|
||||||
|
applicationType: ['', []],
|
||||||
|
authMethodType: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnInit(): void {
|
||||||
|
this.subscription = this.route.params.subscribe(params => this.getData(params));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy(): void {
|
||||||
|
this.subscription?.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getData({ projectid }: Params): Promise<void> {
|
||||||
|
this.projectId = projectid;
|
||||||
|
this.oidcApp.projectId = projectid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public close(): void {
|
||||||
|
this._location.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
public saveOIDCApp(): void {
|
||||||
|
this.oidcApp.name = this.name?.value;
|
||||||
|
this.oidcApp.applicationType = this.applicationType?.value;
|
||||||
|
this.oidcApp.grantTypesList = this.grantTypesList?.value;
|
||||||
|
this.oidcApp.responseTypesList = this.responseTypesList?.value;
|
||||||
|
this.oidcApp.authMethodType = this.authMethodType?.value;
|
||||||
|
|
||||||
|
console.log(this.oidcApp);
|
||||||
|
|
||||||
|
this.projectService
|
||||||
|
.CreateOIDCApp(this.oidcApp)
|
||||||
|
.then((data: Application) => {
|
||||||
|
this.showSavedDialog(data.toObject());
|
||||||
|
})
|
||||||
|
.catch(data => {
|
||||||
|
this.toast.showError(data.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public showSavedDialog(app: Application.AsObject): void {
|
||||||
|
if (app.oidcConfig !== undefined) {
|
||||||
|
const dialogRef = this.dialog.open(AppSecretDialogComponent, {
|
||||||
|
data: app.oidcConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef.afterClosed().subscribe(result => {
|
||||||
|
console.log('The dialog was closed');
|
||||||
|
this.router.navigate(['projects', this.projectId, 'apps', app.id]);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.router.navigate(['projects', this.projectId, 'apps', app.id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public addUri(event: MatChipInputEvent, target: string): void {
|
||||||
|
const input = event.input;
|
||||||
|
const value = event.value.trim();
|
||||||
|
|
||||||
|
if (value !== '') {
|
||||||
|
if (target === 'REDIRECT') {
|
||||||
|
this.oidcApp.redirectUrisList.push(value);
|
||||||
|
} else if (target === 'POSTREDIRECT') {
|
||||||
|
this.oidcApp.postLogoutRedirectUrisList.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input) {
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeUri(uri: string, target: string): void {
|
||||||
|
if (target === 'REDIRECT') {
|
||||||
|
const index = this.oidcApp.redirectUrisList.indexOf(uri);
|
||||||
|
|
||||||
|
if (index !== undefined && index >= 0) {
|
||||||
|
this.oidcApp.redirectUrisList.splice(index, 1);
|
||||||
|
}
|
||||||
|
} else if (target === 'POSTREDIRECT') {
|
||||||
|
const index = this.oidcApp.postLogoutRedirectUrisList.indexOf(uri);
|
||||||
|
|
||||||
|
if (index !== undefined && index >= 0) {
|
||||||
|
this.oidcApp.postLogoutRedirectUrisList.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
get name(): AbstractControl | null {
|
||||||
|
return this.form.get('name');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
get responseTypesList(): AbstractControl | null {
|
||||||
|
return this.form.get('responseTypesList');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
get grantTypesList(): AbstractControl | null {
|
||||||
|
return this.form.get('grantTypesList');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
get applicationType(): AbstractControl | null {
|
||||||
|
return this.form.get('applicationType');
|
||||||
|
}
|
||||||
|
|
||||||
|
get authMethodType(): AbstractControl | null {
|
||||||
|
return this.form.get('authMethodType');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
121
console/src/app/pages/apps/app-detail/app-detail.component.html
Normal file
121
console/src/app/pages/apps/app-detail/app-detail.component.html
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
<div class="max-width-container">
|
||||||
|
<div class="head">
|
||||||
|
<a (click)="navigateBack()" mat-icon-button>
|
||||||
|
<mat-icon class="icon">arrow_back</mat-icon>
|
||||||
|
</a>
|
||||||
|
<h1>{{ 'APP.PAGES.TITLE' | translate }} {{app?.name}}</h1>
|
||||||
|
|
||||||
|
<p class="desc">{{ 'APP.PAGES.DESCRIPTION' | translate }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<app-card title="{{ 'APP.PAGES.DETAIL.TITLE' | translate }}" *ngIf="app">
|
||||||
|
<form [formGroup]="appNameForm" (ngSubmit)="saveOIDCApp()">
|
||||||
|
<div class="content">
|
||||||
|
<mat-button-toggle-group formControlName="state" class="toggle" (change)="changeState($event)">
|
||||||
|
<mat-button-toggle [value]="AppState.APPSTATE_INACTIVE" matTooltip="Deactivate Org">
|
||||||
|
<mat-icon svgIcon="mdi_light_off"></mat-icon>
|
||||||
|
{{'APP.PAGES.DETAIL.STATE.'+AppState.APPSTATE_INACTIVE | translate}}
|
||||||
|
</mat-button-toggle>
|
||||||
|
<mat-button-toggle [value]="AppState.APPSTATE_ACTIVE" matTooltip="Activate Org">
|
||||||
|
<mat-icon svgIcon="mdi_light_on"></mat-icon>
|
||||||
|
{{'APP.PAGES.DETAIL.STATE.'+AppState.APPSTATE_ACTIVE | translate}}
|
||||||
|
</mat-button-toggle>
|
||||||
|
</mat-button-toggle-group>
|
||||||
|
|
||||||
|
<mat-form-field class="formfield">
|
||||||
|
<mat-label>{{ 'APP.NAME' | translate }}</mat-label>
|
||||||
|
<input matInput formControlName="name" />
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div class="btn-container">
|
||||||
|
<button type="submit" color="primary" [disabled]="appNameForm.invalid || name?.disabled"
|
||||||
|
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</app-card>
|
||||||
|
|
||||||
|
<app-card title="{{ 'APP.OIDC.TITLE' | translate }}" *ngIf="app && app.oidcConfig">
|
||||||
|
<card-actions class="card-actions">
|
||||||
|
<button mat-stroked-button
|
||||||
|
(click)="regenerateOIDCClientSecret()">{{'APP.OIDC.REGENERATESECRET' | translate}}</button>
|
||||||
|
</card-actions>
|
||||||
|
<form *ngIf="appForm" [formGroup]="appForm" (ngSubmit)="saveOIDCApp()">
|
||||||
|
<div class="content">
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>{{ 'APP.OIDC.CLIENTID' | translate }}</mat-label>
|
||||||
|
<input matInput formControlName="clientId" />
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>{{ 'APP.OIDC.RESPONSE' | translate }}</mat-label>
|
||||||
|
<mat-select formControlName="responseTypesList" multiple>
|
||||||
|
<mat-option *ngFor="let type of oidcResponseTypes" [value]="type">
|
||||||
|
{{ 'APP.OIDC.RESPONSE'+type | translate }}
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>{{ 'APP.OIDC.GRANT' | translate }}</mat-label>
|
||||||
|
<mat-select formControlName="grantTypesList" multiple>
|
||||||
|
<mat-option *ngFor="let grant of oidcGrantTypes" [value]="grant">
|
||||||
|
{{ 'APP.OIDC.GRANT'+grant | translate }}
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="formfield">
|
||||||
|
<mat-label>{{ 'APP.OIDC.APPTYPE' | translate }}</mat-label>
|
||||||
|
<mat-select formControlName="applicationType">
|
||||||
|
<mat-option *ngFor="let type of oidcAppTypes" [value]="type">
|
||||||
|
{{ 'APP.OIDC.APPTYPE'+type | translate }}
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="formfield">
|
||||||
|
<mat-label>{{ 'APP.OIDC.AUTHMETHOD' | translate }}</mat-label>
|
||||||
|
<mat-select formControlName="authMethodType">
|
||||||
|
<mat-option *ngFor="let type of oidcAuthMethodType" [value]="type">
|
||||||
|
{{ 'APP.OIDC.AUTHMETHOD'+type | translate }}
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field class="full-width" appearance="outline">
|
||||||
|
<mat-label>{{ 'APP.OIDC.REDIRECT' | translate }}</mat-label>
|
||||||
|
<mat-chip-list #chipRedirectList>
|
||||||
|
<mat-chip *ngFor="let redirect of redirectUrisList" [selectable]="selectable"
|
||||||
|
(removed)="remove(redirect, RedirectType.REDIRECT)">
|
||||||
|
{{redirect}}
|
||||||
|
<mat-icon matChipRemove *ngIf="removable">cancel</mat-icon>
|
||||||
|
</mat-chip>
|
||||||
|
<input [matChipInputFor]="chipRedirectList" [matChipInputSeparatorKeyCodes]="separatorKeysCodes"
|
||||||
|
[matChipInputAddOnBlur]="addOnBlur"
|
||||||
|
(matChipInputTokenEnd)="add($event, RedirectType.REDIRECT)">
|
||||||
|
</mat-chip-list>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field class="full-width" appearance="outline">
|
||||||
|
<mat-label>{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}</mat-label>
|
||||||
|
<mat-chip-list #chipPostRedirectList>
|
||||||
|
<mat-chip *ngFor="let redirect of postLogoutRedirectUrisList" [selectable]="selectable"
|
||||||
|
(removed)="remove(redirect, RedirectType.POSTREDIRECT)">
|
||||||
|
{{redirect}}
|
||||||
|
<mat-icon matChipRemove *ngIf="removable">cancel</mat-icon>
|
||||||
|
</mat-chip>
|
||||||
|
<input [matChipInputFor]="chipPostRedirectList"
|
||||||
|
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" [matChipInputAddOnBlur]="addOnBlur"
|
||||||
|
(matChipInputTokenEnd)="add($event, RedirectType.POSTREDIRECT)">
|
||||||
|
</mat-chip-list>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div class="btn-container">
|
||||||
|
|
||||||
|
<button type="submit" color="primary" [disabled]="appForm.invalid"
|
||||||
|
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</app-card>
|
||||||
|
</div>
|
@ -0,0 +1,73 @@
|
|||||||
|
.head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid #ffffff20;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin: 0 1rem;
|
||||||
|
margin-left: 2rem;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
font-size: .9rem;
|
||||||
|
color: #81868a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
button {
|
||||||
|
border-radius: .5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin: 0 -.5rem;
|
||||||
|
|
||||||
|
&.nowrap{
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-form-field {
|
||||||
|
flex: 1 1 30%;
|
||||||
|
margin: 0 .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-width {
|
||||||
|
flex-basis: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
align-self: flex-start;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
border-radius: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin: 0 -.5rem;
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: .5rem;
|
||||||
|
margin: 0 .5rem;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AppDetailComponent } from './app-detail.component';
|
||||||
|
|
||||||
|
describe('AppDetailComponent', () => {
|
||||||
|
let component: AppDetailComponent;
|
||||||
|
let fixture: ComponentFixture<AppDetailComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [AppDetailComponent],
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(AppDetailComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
262
console/src/app/pages/apps/app-detail/app-detail.component.ts
Normal file
262
console/src/app/pages/apps/app-detail/app-detail.component.ts
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes';
|
||||||
|
import { Location } from '@angular/common';
|
||||||
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||||
|
import { MatButtonToggleChange } from '@angular/material/button-toggle';
|
||||||
|
import { MatChipInputEvent } from '@angular/material/chips';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { ActivatedRoute, Params } from '@angular/router';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
import {
|
||||||
|
Application,
|
||||||
|
AppState,
|
||||||
|
OIDCApplicationType,
|
||||||
|
OIDCAuthMethodType,
|
||||||
|
OIDCConfig,
|
||||||
|
OIDCGrantType,
|
||||||
|
OIDCResponseType,
|
||||||
|
} from 'src/app/proto/generated/management_pb';
|
||||||
|
import { ProjectService } from 'src/app/services/project.service';
|
||||||
|
import { ToastService } from 'src/app/services/toast.service';
|
||||||
|
|
||||||
|
import { AppSecretDialogComponent } from '../app-secret-dialog/app-secret-dialog.component';
|
||||||
|
|
||||||
|
enum RedirectType {
|
||||||
|
REDIRECT = 'redirect',
|
||||||
|
POSTREDIRECT = 'postredirect',
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-app-detail',
|
||||||
|
templateUrl: './app-detail.component.html',
|
||||||
|
styleUrls: ['./app-detail.component.scss'],
|
||||||
|
})
|
||||||
|
export class AppDetailComponent implements OnInit, OnDestroy {
|
||||||
|
public selectable: boolean = false;
|
||||||
|
public removable: boolean = true;
|
||||||
|
public addOnBlur: boolean = true;
|
||||||
|
public readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
|
||||||
|
|
||||||
|
private subscription?: Subscription;
|
||||||
|
public projectId: string = '';
|
||||||
|
public app!: Application.AsObject;
|
||||||
|
public oidcResponseTypes: OIDCResponseType[] = [
|
||||||
|
OIDCResponseType.OIDCRESPONSETYPE_CODE,
|
||||||
|
OIDCResponseType.OIDCRESPONSETYPE_ID_TOKEN,
|
||||||
|
OIDCResponseType.OIDCRESPONSETYPE_TOKEN,
|
||||||
|
];
|
||||||
|
public oidcGrantTypes: OIDCGrantType[] = [
|
||||||
|
OIDCGrantType.OIDCGRANTTYPE_AUTHORIZATION_CODE,
|
||||||
|
OIDCGrantType.OIDCGRANTTYPE_IMPLICIT,
|
||||||
|
OIDCGrantType.OIDCGRANTTYPE_REFRESH_TOKEN,
|
||||||
|
];
|
||||||
|
public oidcAppTypes: OIDCApplicationType[] = [
|
||||||
|
OIDCApplicationType.OIDCAPPLICATIONTYPE_WEB,
|
||||||
|
OIDCApplicationType.OIDCAPPLICATIONTYPE_USER_AGENT,
|
||||||
|
OIDCApplicationType.OIDCAPPLICATIONTYPE_NATIVE,
|
||||||
|
];
|
||||||
|
|
||||||
|
public oidcAuthMethodType: OIDCAuthMethodType[] = [
|
||||||
|
OIDCAuthMethodType.OIDCAUTHMETHODTYPE_BASIC,
|
||||||
|
OIDCAuthMethodType.OIDCAUTHMETHODTYPE_POST,
|
||||||
|
OIDCAuthMethodType.OIDCAUTHMETHODTYPE_NONE,
|
||||||
|
];
|
||||||
|
|
||||||
|
public AppState: any = AppState;
|
||||||
|
public appNameForm!: FormGroup;
|
||||||
|
public appForm!: FormGroup;
|
||||||
|
public redirectUrisList: string[] = [];
|
||||||
|
public postLogoutRedirectUrisList: string[] = [];
|
||||||
|
|
||||||
|
public RedirectType: any = RedirectType;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public translate: TranslateService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private toast: ToastService,
|
||||||
|
private projectService: ProjectService,
|
||||||
|
private fb: FormBuilder,
|
||||||
|
private _location: Location,
|
||||||
|
private dialog: MatDialog,
|
||||||
|
) {
|
||||||
|
this.appNameForm = this.fb.group({
|
||||||
|
state: ['', []],
|
||||||
|
name: ['', [Validators.required]],
|
||||||
|
});
|
||||||
|
this.appForm = this.fb.group({
|
||||||
|
clientId: [{ value: '', disabled: true }],
|
||||||
|
responseTypesList: [],
|
||||||
|
grantTypesList: [],
|
||||||
|
applicationType: [],
|
||||||
|
authMethodType: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnInit(): void {
|
||||||
|
this.subscription = this.route.params.subscribe(params => this.getData(params));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy(): void {
|
||||||
|
this.subscription?.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getData({ projectid, id }: Params): Promise<void> {
|
||||||
|
this.projectId = projectid;
|
||||||
|
this.app = (await this.projectService.GetApplicationById(projectid, id)).toObject();
|
||||||
|
this.appNameForm.patchValue(this.app);
|
||||||
|
if (this.app.state !== AppState.APPSTATE_ACTIVE) {
|
||||||
|
this.appNameForm.controls['name'].disable();
|
||||||
|
this.appForm.disable();
|
||||||
|
} else {
|
||||||
|
this.appNameForm.controls['name'].enable();
|
||||||
|
this.appForm.enable();
|
||||||
|
this.clientId?.disable();
|
||||||
|
}
|
||||||
|
if (this.app.oidcConfig?.redirectUrisList) {
|
||||||
|
this.redirectUrisList = this.app.oidcConfig.redirectUrisList;
|
||||||
|
}
|
||||||
|
if (this.app.oidcConfig?.postLogoutRedirectUrisList) {
|
||||||
|
this.postLogoutRedirectUrisList = this.app.oidcConfig.postLogoutRedirectUrisList;
|
||||||
|
}
|
||||||
|
if (this.app.oidcConfig) {
|
||||||
|
this.appForm.patchValue(this.app.oidcConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public changeState(event: MatButtonToggleChange): void {
|
||||||
|
if (event.value === AppState.APPSTATE_ACTIVE) {
|
||||||
|
this.projectService.ReactivateApplication(this.app.id).then(() => {
|
||||||
|
this.toast.showInfo('Reactivated Application');
|
||||||
|
}).catch((error: any) => {
|
||||||
|
this.toast.showError(error.message);
|
||||||
|
});
|
||||||
|
} else if (event.value === AppState.APPSTATE_INACTIVE) {
|
||||||
|
this.projectService.DectivateApplication(this.app.id).then(() => {
|
||||||
|
this.toast.showInfo('Deactivated Application');
|
||||||
|
}).catch((error: any) => {
|
||||||
|
this.toast.showError(error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.value !== AppState.APPSTATE_ACTIVE) {
|
||||||
|
this.appNameForm.controls['name'].disable();
|
||||||
|
this.appForm.disable();
|
||||||
|
} else {
|
||||||
|
this.appNameForm.controls['name'].enable();
|
||||||
|
this.appForm.enable();
|
||||||
|
this.clientId?.disable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public add(event: MatChipInputEvent, target: RedirectType): void {
|
||||||
|
if (target === RedirectType.POSTREDIRECT) {
|
||||||
|
const input = event.input;
|
||||||
|
if (event.value !== '' && event.value !== ' ' && event.value !== '/') {
|
||||||
|
this.postLogoutRedirectUrisList.push(event.value);
|
||||||
|
}
|
||||||
|
if (input) {
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
} else if (target === RedirectType.REDIRECT) {
|
||||||
|
const input = event.input;
|
||||||
|
if (event.value !== '' && event.value !== ' ' && event.value !== '/') {
|
||||||
|
this.redirectUrisList.push(event.value);
|
||||||
|
}
|
||||||
|
if (input) {
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public remove(redirect: any, target: RedirectType): void {
|
||||||
|
|
||||||
|
if (target === RedirectType.POSTREDIRECT) {
|
||||||
|
const index = this.postLogoutRedirectUrisList.indexOf(redirect);
|
||||||
|
|
||||||
|
if (index >= 0) {
|
||||||
|
this.postLogoutRedirectUrisList.splice(index, 1);
|
||||||
|
}
|
||||||
|
} else if (target === RedirectType.REDIRECT) {
|
||||||
|
const index = this.redirectUrisList.indexOf(redirect);
|
||||||
|
|
||||||
|
if (index >= 0) {
|
||||||
|
this.redirectUrisList.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public saveOIDCApp(): void {
|
||||||
|
if (this.appNameForm.valid) {
|
||||||
|
this.app.name = this.name?.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.appForm.valid) {
|
||||||
|
if (this.app.oidcConfig) {
|
||||||
|
this.app.oidcConfig.responseTypesList = this.responseTypesList?.value;
|
||||||
|
this.app.oidcConfig.grantTypesList = this.grantTypesList?.value;
|
||||||
|
this.app.oidcConfig.applicationType = this.applicationType?.value;
|
||||||
|
this.app.oidcConfig.authMethodType = this.authMethodType?.value;
|
||||||
|
this.app.oidcConfig.redirectUrisList = this.redirectUrisList;
|
||||||
|
this.app.oidcConfig.postLogoutRedirectUrisList = this.postLogoutRedirectUrisList;
|
||||||
|
|
||||||
|
console.log(this.app.oidcConfig);
|
||||||
|
|
||||||
|
this.projectService
|
||||||
|
.UpdateOIDCAppConfig(this.projectId, this.app.id, this.app.oidcConfig)
|
||||||
|
.then((data: OIDCConfig) => {
|
||||||
|
this.toast.showInfo('OIDC Config saved');
|
||||||
|
})
|
||||||
|
.catch(data => {
|
||||||
|
this.toast.showError(data.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public regenerateOIDCClientSecret(): void {
|
||||||
|
this.projectService.RegenerateOIDCClientSecret(this.app.id).then((data: OIDCConfig) => {
|
||||||
|
console.log(data.toObject());
|
||||||
|
this.toast.showInfo('OIDC Secret Regenerated');
|
||||||
|
this.dialog.open(AppSecretDialogComponent, {
|
||||||
|
data: {
|
||||||
|
clientId: data.toObject().clientId,
|
||||||
|
clientSecret: data.toObject().clientSecret,
|
||||||
|
},
|
||||||
|
width: '400px',
|
||||||
|
});
|
||||||
|
|
||||||
|
}).catch(data => {
|
||||||
|
this.toast.showError(data.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public navigateBack(): void {
|
||||||
|
this._location.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
public get name(): AbstractControl | null {
|
||||||
|
return this.appNameForm.get('name');
|
||||||
|
}
|
||||||
|
|
||||||
|
public get clientId(): AbstractControl | null {
|
||||||
|
return this.appForm.get('clientId');
|
||||||
|
}
|
||||||
|
|
||||||
|
public get responseTypesList(): AbstractControl | null {
|
||||||
|
return this.appForm.get('responseTypesList');
|
||||||
|
}
|
||||||
|
|
||||||
|
public get grantTypesList(): AbstractControl | null {
|
||||||
|
return this.appForm.get('grantTypesList');
|
||||||
|
}
|
||||||
|
|
||||||
|
public get applicationType(): AbstractControl | null {
|
||||||
|
return this.appForm.get('applicationType');
|
||||||
|
}
|
||||||
|
|
||||||
|
public get authMethodType(): AbstractControl | null {
|
||||||
|
return this.appForm.get('authMethodType');
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
<h1 mat-dialog-title>
|
||||||
|
<span class="title">{{'APP.OIDC.CLIENTSECRET' | translate}}</span>
|
||||||
|
</h1>
|
||||||
|
<p class="desc">{{'APP.OIDC.CLIENTSECRET_DESCRIPTION' | translate}}</p>
|
||||||
|
<div mat-dialog-content>
|
||||||
|
<p *ngIf="data.clientId">ClientId: {{data.clientId}}</p>
|
||||||
|
|
||||||
|
<div *ngIf="data.clientSecret" class="flex">
|
||||||
|
<button matTooltip="copy to clipboard" (click)="copytoclipboard(data.clientSecret)" mat-icon-button>
|
||||||
|
<mat-icon *ngIf="!copied" svgIcon="mdi_content_copy"></mat-icon>
|
||||||
|
<mat-icon *ngIf="copied">check</mat-icon>
|
||||||
|
|
||||||
|
</button>
|
||||||
|
<span class="secret">{{data.clientSecret}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div mat-dialog-actions class="action">
|
||||||
|
|
||||||
|
<button cdkFocusInitial color="primary" mat-raised-button class="ok-button" (click)="closeDialog()">
|
||||||
|
{{'ACTIONS.CLOSE' | translate}}
|
||||||
|
</button>
|
||||||
|
</div>
|
@ -0,0 +1,37 @@
|
|||||||
|
.title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc {
|
||||||
|
color: #81868a;
|
||||||
|
font-size: .9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-width {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
.ok-button {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: .5rem;
|
||||||
|
border: 1px solid #ffffff20;
|
||||||
|
border-radius: .5rem;
|
||||||
|
|
||||||
|
.secret {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AppSecretDialogComponent } from './app-secret-dialog.component';
|
||||||
|
|
||||||
|
describe('AppSecretDialogComponent', () => {
|
||||||
|
let component: AppSecretDialogComponent;
|
||||||
|
let fixture: ComponentFixture<AppSecretDialogComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [AppSecretDialogComponent],
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(AppSecretDialogComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,35 @@
|
|||||||
|
import { Component, Inject } from '@angular/core';
|
||||||
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-app-secret-dialog',
|
||||||
|
templateUrl: './app-secret-dialog.component.html',
|
||||||
|
styleUrls: ['./app-secret-dialog.component.scss'],
|
||||||
|
})
|
||||||
|
export class AppSecretDialogComponent {
|
||||||
|
public copied: boolean = false;
|
||||||
|
constructor(public dialogRef: MatDialogRef<AppSecretDialogComponent>,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: any) { }
|
||||||
|
|
||||||
|
public closeDialog(): void {
|
||||||
|
this.dialogRef.close(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public copytoclipboard(value: string): void {
|
||||||
|
const selBox = document.createElement('textarea');
|
||||||
|
selBox.style.position = 'fixed';
|
||||||
|
selBox.style.left = '0';
|
||||||
|
selBox.style.top = '0';
|
||||||
|
selBox.style.opacity = '0';
|
||||||
|
selBox.value = value;
|
||||||
|
document.body.appendChild(selBox);
|
||||||
|
selBox.focus();
|
||||||
|
selBox.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(selBox);
|
||||||
|
this.copied = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.copied = false;
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}
|
24
console/src/app/pages/apps/apps-routing.module.ts
Normal file
24
console/src/app/pages/apps/apps-routing.module.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
|
||||||
|
import { AppCreateComponent } from '../apps/app-create/app-create.component';
|
||||||
|
import { AppDetailComponent } from '../apps/app-detail/app-detail.component';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: 'create',
|
||||||
|
component: AppCreateComponent,
|
||||||
|
data: { animation: 'AddPage' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':id',
|
||||||
|
component: AppDetailComponent,
|
||||||
|
data: { animation: 'HomePage' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule],
|
||||||
|
})
|
||||||
|
export class AppsRoutingModule { }
|
69
console/src/app/pages/apps/apps.module.ts
Normal file
69
console/src/app/pages/apps/apps.module.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
||||||
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||||
|
import { MatChipsModule } from '@angular/material/chips';
|
||||||
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
|
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||||
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { HttpLoaderFactory } from 'src/app/app.module';
|
||||||
|
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
|
||||||
|
import { CardModule } from 'src/app/modules/card/card.module';
|
||||||
|
|
||||||
|
import { AppCreateComponent } from './app-create/app-create.component';
|
||||||
|
import { AppDetailComponent } from './app-detail/app-detail.component';
|
||||||
|
import { AppSecretDialogComponent } from './app-secret-dialog/app-secret-dialog.component';
|
||||||
|
import { AppsRoutingModule } from './apps-routing.module';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AppCreateComponent,
|
||||||
|
AppDetailComponent,
|
||||||
|
AppSecretDialogComponent,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
AppsRoutingModule,
|
||||||
|
FormsModule,
|
||||||
|
TranslateModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
HasRoleModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatMenuModule,
|
||||||
|
MatChipsModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatSelectModule,
|
||||||
|
MatButtonToggleModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatProgressSpinnerModule,
|
||||||
|
MatProgressBarModule,
|
||||||
|
MatDialogModule,
|
||||||
|
MatCheckboxModule,
|
||||||
|
CardModule,
|
||||||
|
MatTooltipModule,
|
||||||
|
TranslateModule.forChild({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useFactory: HttpLoaderFactory,
|
||||||
|
deps: [HttpClient],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
entryComponents: [
|
||||||
|
AppSecretDialogComponent,
|
||||||
|
],
|
||||||
|
exports: [TranslateModule],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA],
|
||||||
|
})
|
||||||
|
export class AppsModule { }
|
18
console/src/app/pages/home/home-routing.module.ts
Normal file
18
console/src/app/pages/home/home-routing.module.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
|
||||||
|
import { HomeComponent } from './home.component';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: HomeComponent,
|
||||||
|
data: { animation: 'HomePage' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule],
|
||||||
|
})
|
||||||
|
export class HomeRoutingModule { }
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user