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:
|
||||
- package_manager: "go:modules"
|
||||
directory: "/"
|
||||
update_schedule: "daily"
|
||||
update_schedule: "weekly"
|
||||
commit_message:
|
||||
prefix: "chore"
|
||||
include_scope: true
|
||||
- package_manager: "javascript"
|
||||
directory: "/console"
|
||||
update_schedule: "weekly"
|
||||
commit_message:
|
||||
prefix: "chore"
|
||||
include_scope: true
|
97
.github/workflows/release.yml
vendored
97
.github/workflows/release.yml
vendored
@ -10,24 +10,51 @@ env:
|
||||
|
||||
jobs:
|
||||
|
||||
angular: # TODO Implement proper build and cache and coverage upload
|
||||
angular-test: # 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: echo "hodor" > hodor.txt
|
||||
# - run: npm ci
|
||||
# - run: npm run lint
|
||||
# - run: npm run prodbuild
|
||||
# - run: npm test
|
||||
- run: npm ci
|
||||
#- run: npm test
|
||||
- run: echo "replace me with real 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
|
||||
with:
|
||||
name: angular
|
||||
path: hodor.txt
|
||||
path: console/dist/console
|
||||
|
||||
go: # TODO Implement proper build and cache
|
||||
go-test:
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@ -35,37 +62,59 @@ jobs:
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
- run: go test -race -v -coverprofile=profile.cov ./...
|
||||
- run: go build -o zitadel cmd/zitadel/main.go
|
||||
- uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: go-coverage
|
||||
path: profile.cov
|
||||
- uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: go-binary
|
||||
path: zitadel
|
||||
- uses: codecov/codecov-action@v1
|
||||
with:
|
||||
file: ./profile.cov
|
||||
name: codecov-go
|
||||
|
||||
container-prod: # Artifact paths need better place
|
||||
go-lint:
|
||||
runs-on: ubuntu-18.04
|
||||
needs: [angular, go]
|
||||
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
|
||||
with:
|
||||
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
|
||||
with:
|
||||
name: go-binary
|
||||
path: .build/go
|
||||
- uses: docker/build-push-action@v1
|
||||
with:
|
||||
dockerfile: build/dockerfile-prod
|
||||
dockerfile: build/docker/prod
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ github.token }}
|
||||
registry: ${{ env.REGISTRY }}
|
||||
@ -75,7 +124,7 @@ jobs:
|
||||
|
||||
container-vulnerability-scan:
|
||||
runs-on: ubuntu-18.04
|
||||
needs: [container-prod]
|
||||
needs: container-prod
|
||||
steps:
|
||||
- name: Source checkout
|
||||
uses: actions/checkout@v2
|
||||
@ -89,7 +138,7 @@ jobs:
|
||||
- uses: anchore/scan-action@master
|
||||
with:
|
||||
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
|
||||
- name: anchore inline scan JSON results
|
||||
run: for j in `ls ./anchore-reports/*.json`; do echo "---- ${j} ----"; cat ${j}; echo; done
|
||||
@ -98,9 +147,9 @@ jobs:
|
||||
name: anchore-reports
|
||||
path: ./anchore-reports/
|
||||
|
||||
container-test: # TODO Implement proper test
|
||||
container-test:
|
||||
runs-on: ubuntu-18.04
|
||||
needs: [container-prod]
|
||||
needs: container-prod
|
||||
steps:
|
||||
- name: Source checkout
|
||||
uses: actions/checkout@v2
|
||||
@ -112,7 +161,7 @@ jobs:
|
||||
- name: Docker Login
|
||||
run: docker login $REGISTRY -u $GITHUB_ACTOR -p $GITHUB_TOKEN
|
||||
- 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:
|
||||
runs-on: ubuntu-18.04
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -28,6 +28,7 @@ key.json
|
||||
.keys/*
|
||||
|
||||
cockroach-data/*
|
||||
.build/
|
||||
|
||||
#binaries
|
||||
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