Merge branch 'master' into new-eventstore

This commit is contained in:
adlerhurst
2020-10-27 16:07:24 +01:00
809 changed files with 48599 additions and 24100 deletions

View File

@@ -24,3 +24,11 @@ updates:
commit-message: commit-message:
prefix: chore prefix: chore
include: scope include: scope
- package-ecosystem: npm
directory: "/site"
schedule:
interval: monthly
open-pull-requests-limit: 10
commit-message:
prefix: chore
include: scope

View File

@@ -3,9 +3,13 @@ name: "Code scanning - action"
on: on:
push: push:
branches: [master, ] branches: [master, ]
paths-ignore:
- 'site/**'
pull_request: pull_request:
# The branches below must be a subset of the branches above # The branches below must be a subset of the branches above
branches: [master] branches: [master]
paths-ignore:
- 'site/**'
schedule: schedule:
- cron: '0 12 * * 2' - cron: '0 12 * * 2'

View File

@@ -47,4 +47,4 @@ jobs:
ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
BRANCH: gh-pages BRANCH: gh-pages
FOLDER: site/__sapper__/export FOLDER: site/__sapper__/export
CLEAN: true CLEAN: true

View File

@@ -5,7 +5,7 @@ env:
GITHUB_TOKEN: ${{ secrets.CR_PAT }} GITHUB_TOKEN: ${{ secrets.CR_PAT }}
REGISTRY: ghcr.io REGISTRY: ghcr.io
NODE_VERSION: '12' NODE_VERSION: '12'
GO_VERSION: '1.14' GO_VERSION: '1.15'
jobs: jobs:
@@ -126,30 +126,6 @@ jobs:
repository: ${{ github.repository }} repository: ${{ github.repository }}
tag_with_ref: true tag_with_ref: true
tag_with_sha: true tag_with_sha: true
container-vulnerability-scan:
runs-on: ubuntu-18.04
needs: container-prod
steps:
- name: Source checkout
uses: actions/checkout@v2
- name: Generate Short SHA Container Tag
id: vars
run: echo "::set-output name=sha_short::SHA-$(git rev-parse --short HEAD)"
- name: Check outputs
run: echo ${{ steps.vars.outputs.sha_short }}
- name: Docker Login
run: docker login $REGISTRY -u $GITHUB_ACTOR -p $GITHUB_TOKEN
- uses: anchore/scan-action@master
with:
image-reference: "${{ env.REGISTRY }}/${{ github.repository }}:${{ steps.vars.outputs.sha_short }}"
dockerfile-path: "./build/docker/Dockerfile"
fail-build: false
acs-report-enable: true
- name: Upload Anchore Scan Report
uses: github/codeql-action/upload-sarif@v1
with:
sarif_file: results.sarif
release: release:
runs-on: ubuntu-18.04 runs-on: ubuntu-18.04
@@ -191,4 +167,4 @@ jobs:
if: env.CAOS_NEXT_VERSION != '' if: env.CAOS_NEXT_VERSION != ''
- name: Docker Push Latest - name: Docker Push Latest
run: docker push $REGISTRY/$GITHUB_REPOSITORY:latest run: docker push $REGISTRY/$GITHUB_REPOSITORY:latest
if: env.CAOS_NEXT_VERSION != '' if: env.CAOS_NEXT_VERSION != ''

19
.github/workflows/spellcheck.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
name: Spellcheck
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
spellcheck:
name: Typo CI (GitHub Action)
runs-on: ubuntu-latest
timeout-minutes: 4
if: "!contains(github.event.head_commit.message, '[ci skip]')"
steps:
- name: TypoCheck
uses: typoci/spellcheck-action@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

40
.typo-ci.yml Normal file
View File

@@ -0,0 +1,40 @@
# What language dictionaries should it use? Currently Typo CI supports:
# de
# en
# en_GB
# es
# fr
# it
# pt
# pt_BR
dictionaries:
- en
- en_GB
- de
# Any files/folders we should ignore?
excluded_files:
- ".codecov/*"
- ".github/*"
- "build/*"
- "k8s/*"
- "*.min.css"
- "*.css.map"
- "*.min.js"
- "*.js.map"
- "package-lock.json"
- "package.json"
- ".releaserc.js"
- ".typo-ci.yml"
- ".gitignore"
- "go.mod"
- "go.sum"
# Any typos we should ignore?
excluded_words:
- typoci
- idps
- ZITADEL's
# Would you like filenames to also be spellchecked?
spellcheck_filenames: false

11
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,11 @@
# How to contribute to ZITADEL
## **Did you find a bug?**
## **Did you find a security flaw?**
* Please read [Security Policy](SECURITY.md).
## **Do you want to contribute to the ZITADEL documentation?**
* Please read [Contributing to the ZITADEL Documentation](site/CONTRIBUTING.md).

View File

@@ -1,4 +1,4 @@
<img src="./docs/img/zitadel-logo-dark@2x.png" alt="Zitadel Logo" height="100px" width="auto" /> <img src="./site/static/logos/zitadel-logo-dark@2x.png" alt="Zitadel Logo" height="100px" width="auto" />
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
[![Release](https://github.com/caos/zitadel/workflows/Release/badge.svg)](https://github.com/caos/zitadel/actions) [![Release](https://github.com/caos/zitadel/workflows/Release/badge.svg)](https://github.com/caos/zitadel/actions)
@@ -7,48 +7,49 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/caos/zitadel)](https://goreportcard.com/report/github.com/caos/zitadel) [![Go Report Card](https://goreportcard.com/badge/github.com/caos/zitadel)](https://goreportcard.com/report/github.com/caos/zitadel)
[![codecov](https://codecov.io/gh/caos/zitadel/branch/master/graph/badge.svg)](https://codecov.io/gh/caos/zitadel) [![codecov](https://codecov.io/gh/caos/zitadel/branch/master/graph/badge.svg)](https://codecov.io/gh/caos/zitadel)
> This project is in a alpha state. The application will continue breaking until version 1.0.0 is released > This project is in a beta state and API might still change a bit
## What Is It ## What Is It
`ZITADEL` is a Cloud Native Identity and Access Management solution. All server side components are written in `Go` and the management interface, called `Console`, is written in `Angular`. **ZITADEL** is a "Cloud Native Identity and Access Management" solution. All server side components are written in [**Go**](https://golang.org/) and the management interface, called **Console**, is written in [**Angular**](https://angular.io/).
We optimized `ZITADEL` for the usage as `service provider IAM`. By `service provider` we think of companies who build services for e.g SaaS cases. Often these companies would like to use an IAM where they can register their application and grant other people or companies the right to self manage a set of roles within that application. We optimized **ZITADEL** for the usage as "service provider" IAM. By "service provider" we think of companies who build services for e.g SaaS cases. Often these companies would like to use an IAM where they can register their application and grant other people or companies the right to self manage a set of roles within that application.
## How Does It Work ## How Does It Work
We built `ZITADEL` around the idea that the IAM should be easy to deploy and scale. That's why we tried to reduce external systems as much as possible. We built **ZITADEL** around the idea that the IAM should be easy to deploy and scale. That's why we tried to reduce external systems as much as possible.
For example, `ZITADEL` is eventsourced but it does not rely on a pub/sub system to function. Instead we built all the functionality right into one binary. For example, **ZITADEL** is event sourced but it does not rely on a pub/sub system to function. Instead we built all the functionality right into one binary.
`ZITADEL` only needs `Kubernetes` for orchestration and `CockroachDB` as storage. **ZITADEL** only needs [**Kubernetes**](https://kubernetes.io/) for orchestration and [**CockroachDB**](https://www.cockroachlabs.com/) as storage.
## Why Another IAM ## Why Another IAM
In the past we already built a closed sourced IAM and tested multiple others. With most of them we had some issues, either technology, feature, pricing or transparency related in nature. For example we find the idea that security related features like `MFA` should not be hidden behind a paywall or a feature price. In the past we already built a closed sourced IAM and tested multiple others. With most of them we had some issues, either technology, feature, pricing or transparency related in nature. For example we find the idea that security related features like **MFA** should not be hidden behind a paywall or a feature price.
One feature that we often missed, was a solid `audit trail` of all IAM resources. Most systems we saw so far either rely on simple log files or use a short retention for this. One feature that we often missed, was a solid **audit trail** of all IAM resources. Most systems we saw so far either rely on simple log files or use a short retention for this.
## How To Use It ## How To Use It
### Use our free tier ### Use our free tier
Stay tuned, we will publish how you can register an organisation in our cloud offering `zitadel.ch` soon. We provide a shared-cloud ZITADEL system where people can register there own organisation.
Yes we have a free tier! Until end of 2020 we operator under a **early access** model where everything is free.
Go check it out under [zitadel.ch](https://zitadel.ch)
### Run your own IAM ### Run your own IAM
Stay tuned, we will soon publish a guide how you can deploy a `hyperconverged` system with our automation tooling called `ORBOS`. Stay tuned, we will soon publish a guide how you can deploy a **hyperconverged** system with our automation tooling called [**ORBOS**](https://github.com/caos/orbos/).
With [ORBOS](https://github.com/caos/orbos/) you will be able to run `ZITADEL` on `GCE` or `StaticProvider` within 20 minutes. To achieve this, [ORBOS](https://github.com/caos/orbos/) will bootstrap and maintain a `Kubernetes` cluster, essential platform components (logging, metrics, ingress, ...), a secure `CockroachDB` cluster and `ZITADEL` itself. With [**ORBOS**](https://github.com/caos/orbos/) you will be able to run [**Kubernetes**](https://kubernetes.io/) on **GCE** or **StaticProvider** within 20 minutes. To achieve this, [[**ORBOS**](https://github.com/caos/orbos/) will bootstrap and maintain a [**Kubernetes**](https://kubernetes.io/) cluster, essential platform components (logging, metrics, ingress, ...), a secure [**CockroachDB**](https://www.cockroachlabs.com/) cluster and **ZITADEL** itself.
The combination of the tools [ORBOS](https://github.com/caos/orbos/) and `ZITADEL` is what makes the operation easy and scalable. The combination of the tools [**ORBOS**](https://github.com/caos/orbos/) and **ZITADEL** is what makes the operation easy and scalable.
See our progress [here](https://github.com/caos/orbos/pull/256)
## Give me some docs ## Give me some docs
This is work in progess but will change soon. Have a look at our constantly evolving docs page [docs.zitadel.ch](https://docs.zitadel.ch).
## How To Contribute ## How To Contribute
TBA Details need to be announced, but feel free to contribute already. As long as you are okay with accepting to contribute under this projects OSS [License](##License) you are fine.
We already have documentation specific [guidelines](./site/CONTRIBUTING.md).
## Security ## Security
@@ -59,3 +60,4 @@ See the policy [here](./SECURITY.md)
See the exact licensing terms [here](./LICENSE) See the exact licensing terms [here](./LICENSE)
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

View File

@@ -1,5 +0,0 @@
# Build
## Console
## Docker

View File

@@ -23,52 +23,75 @@ Eventstore:
MaxCacheSizeInByte: 10485760 #10mb MaxCacheSizeInByte: 10485760 #10mb
SetUp: SetUp:
GlobalOrg: 'Global' Step1:
IAMProject: 'Zitadel' GlobalOrg: 'Global'
DefaultLoginPolicy: IAMProject: 'Zitadel'
AllowUsernamePassword: true DefaultLoginPolicy:
AllowRegister: true AllowUsernamePassword: true
AllowExternalIdp: true AllowRegister: true
Orgs: AllowExternalIdp: true
- Name: 'Global' Orgs:
Domain: 'global.caos.ch' - Name: 'Global'
Default: true Domain: 'global.caos.ch'
OrgIamPolicy: true Default: true
Users: OrgIamPolicy: true
- FirstName: 'Global Org' Users:
LastName: 'Administrator' - FirstName: 'Global Org'
UserName: 'zitadel-global-org-admin@caos.ch' LastName: 'Administrator'
Email: 'zitadel-global-org-admin@caos.ch' UserName: 'zitadel-global-org-admin@caos.ch'
Password: 'Password1!' Email: 'zitadel-global-org-admin@caos.ch'
Owners: Password: 'Password1!'
- 'zitadel-global-org-admin@caos.ch' Owners:
- Name: 'CAOS AG' - 'zitadel-global-org-admin@caos.ch'
Domain: 'caos.ch' - Name: 'CAOS AG'
Users: Domain: 'caos.ch'
- FirstName: 'Zitadel' Users:
LastName: 'Administrator' - FirstName: 'Zitadel'
UserName: 'zitadel-admin' LastName: 'Administrator'
Email: 'zitadel-admin@caos.ch' UserName: 'zitadel-admin'
Password: 'Password1!' Email: 'zitadel-admin@caos.ch'
Owners: Password: 'Password1!'
- 'zitadel-admin@caos.ch' Owners:
Projects: - 'zitadel-admin@caos.ch'
- Name: 'Zitadel' Projects:
OIDCApps: - Name: 'Zitadel'
- Name: 'Management-API' OIDCApps:
- Name: 'Auth-API' - Name: 'Management-API'
- Name: 'Admin-API' - Name: 'Auth-API'
- Name: 'Zitadel Console' - Name: 'Admin-API'
RedirectUris: - Name: 'Zitadel Console'
- '$ZITADEL_CONSOLE/auth/callback' RedirectUris:
PostLogoutRedirectUris: - '$ZITADEL_CONSOLE/auth/callback'
- '$ZITADEL_CONSOLE/signedout' PostLogoutRedirectUris:
ResponseTypes: - '$ZITADEL_CONSOLE/signedout'
- $ZITADEL_CONSOLE_RESPONSE_TYPE ResponseTypes:
GrantTypes: - $ZITADEL_CONSOLE_RESPONSE_TYPE
- $ZITADEL_CONSOLE_GRANT_TYPE GrantTypes:
ApplicationType: 'USER_AGENT' - $ZITADEL_CONSOLE_GRANT_TYPE
AuthMethodType: 'NONE' ApplicationType: 'USER_AGENT'
DevMode: $ZITADEL_CONSOLE_DEV_MODE AuthMethodType: 'NONE'
Owners: DevMode: $ZITADEL_CONSOLE_DEV_MODE
- 'zitadel-admin@caos.ch' Owners:
- 'zitadel-admin@caos.ch'
Step2:
DefaultPasswordComplexityPolicy:
MinLength: 8
HasLowercase: true
HasUppercase: true
HasSymbol: true
HasNumber: true
Step3:
DefaultPasswordAgePolicy:
MaxAgeDays: 0
ExpireWarnDays: 0
Step4:
DefaultPasswordLockoutPolicy:
MaxAttempts: 5
ShowLockOutFailures: false
Step5:
DefaultOrgIAMPolicy:
UserLoginMustBeDomain: true
Step6:
DefaultLabelPolicy:
PrimaryColor: '#222324'
SecondaryColor: '#ffffff'

View File

@@ -52,28 +52,10 @@ SystemDefaults:
EncryptionKeyID: $ZITADEL_OTP_VERIFICATION_KEY EncryptionKeyID: $ZITADEL_OTP_VERIFICATION_KEY
VerificationLifetimes: VerificationLifetimes:
PasswordCheck: 240h #10d PasswordCheck: 240h #10d
ExternalLoginCheck: 240h #10d
MfaInitSkip: 720h #30d MfaInitSkip: 720h #30d
MfaSoftwareCheck: 18h MfaSoftwareCheck: 18h
MfaHardwareCheck: 12h MfaHardwareCheck: 12h
DefaultPolicies:
Age:
Description: Standard age policy
MaxAgeDays: 365
ExpireWarnDays: 10
Complexity:
Description: Standard complexity policy
MinLength: 8
HasLowercase: true
HasUppercase: false
HasNumber: true
HasSymbol: true
Lockout:
Description: Standard lockout policy
MaxAttempts: 5
ShowLockOutFailures: true
OrgIam:
Description: Standard org policy
UserLoginMustBeDomain: true
IamID: 'IAM' IamID: 'IAM'
DomainVerification: DomainVerification:
VerificationKey: VerificationKey:

View File

@@ -26,7 +26,7 @@
"assets": [ "assets": [
"src/favicon.ico", "src/favicon.ico",
"src/assets", "src/assets",
"src/manifest.webmanifest", "src/manifest.webmanifest"
], ],
"styles": [ "styles": [
"src/styles.scss" "src/styles.scss"
@@ -34,8 +34,9 @@
"scripts": [], "scripts": [],
"allowedCommonJsDependencies": [ "allowedCommonJsDependencies": [
"@angular/common/locales/de", "@angular/common/locales/de",
"src/app/proto/generated/*.js", "src/app/proto/generated/**",
"src/app/proto/generated/**/*.js" "file-saver",
"qrcode"
] ]
}, },
"configurations": { "configurations": {

3593
console/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,7 @@
"@types/file-saver": "^2.0.1", "@types/file-saver": "^2.0.1",
"@types/uuid": "^8.3.0", "@types/uuid": "^8.3.0",
"@types/google-protobuf": "^3.7.3", "@types/google-protobuf": "^3.7.3",
"angularx-qrcode": "^10.0.10", "angularx-qrcode": "^10.0.11",
"angular-oauth2-oidc": "^10.0.3", "angular-oauth2-oidc": "^10.0.3",
"cors": "^2.8.5", "cors": "^2.8.5",
"file-saver": "^2.0.2", "file-saver": "^2.0.2",
@@ -35,34 +35,34 @@
"google-protobuf": "^3.13.0", "google-protobuf": "^3.13.0",
"grpc": "^1.24.3", "grpc": "^1.24.3",
"grpc-web": "^1.2.1", "grpc-web": "^1.2.1",
"moment": "^2.27.0", "moment": "^2.29.1",
"ngx-moment": "^5.0.0", "ngx-moment": "^5.0.0",
"ngx-quicklink": "^0.2.4", "ngx-quicklink": "^0.2.4",
"rxjs": "~6.6.3", "rxjs": "~6.6.3",
"ts-protoc-gen": "^0.12.0", "ts-protoc-gen": "^0.13.0",
"tslib": "^2.0.1", "tslib": "^2.0.3",
"uuid": "^8.3.0", "uuid": "^8.3.1",
"zone.js": "~0.11.1" "zone.js": "~0.11.2"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "~0.1000.8", "@angular-devkit/build-angular": "~0.1002.0",
"@angular/cli": "~10.0.7", "@angular/cli": "~10.2.0",
"@angular/compiler-cli": "~10.0.11", "@angular/compiler-cli": "~10.0.11",
"@types/jasmine": "~3.5.13", "@types/jasmine": "~3.6.0",
"@angular/language-service": "~10.1.0", "@angular/language-service": "~10.2.0",
"@types/jasminewd2": "~2.0.3", "@types/jasminewd2": "~2.0.3",
"@types/node": "^14.6.4", "@types/node": "^14.14.3",
"codelyzer": "^6.0.0", "codelyzer": "^6.0.1",
"jasmine-core": "~3.6.0", "jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0", "jasmine-spec-reporter": "~6.0.0",
"karma": "~5.2.1", "karma": "~5.2.3",
"karma-chrome-launcher": "~3.1.0", "karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~3.0.2", "karma-coverage-istanbul-reporter": "~3.0.2",
"karma-jasmine": "~4.0.1", "karma-jasmine": "~4.0.1",
"karma-jasmine-html-reporter": "^1.5.0", "karma-jasmine-html-reporter": "^1.5.0",
"prettier": "^2.1.1", "prettier": "^2.1.2",
"protractor": "~7.0.0", "protractor": "~7.0.0",
"stylelint": "^13.7.1", "stylelint": "^13.7.2",
"stylelint-config-standard": "^20.0.0", "stylelint-config-standard": "^20.0.0",
"stylelint-scss": "^3.18.0", "stylelint-scss": "^3.18.0",
"ts-node": "~9.0.0", "ts-node": "~9.0.0",

View File

@@ -77,6 +77,38 @@ export const navAnimations: Array<AnimationTriggerMetadata> = [
]), ]),
]; ];
export const enterAnimations: Array<AnimationTriggerMetadata> = [
trigger('appearfade', [
transition(':enter', [
style({
transform: 'scale(.9) translateY(-10%)',
opacity: 0,
}),
animate(
'100ms ease-in-out',
style({
transform: 'scale(1) translateY(0%)',
opacity: 1,
}),
),
]),
transition(':leave', [
style({
transform: 'scale(1) translateY(0%)',
opacity: 1,
}),
animate(
'100ms ease-in-out',
style({
transform: 'scale(.9) translateY(-10%)',
opacity: 0,
}),
),
]),
]),
];
export const routeAnimations: AnimationTriggerMetadata = trigger('routeAnimations', [ export const routeAnimations: AnimationTriggerMetadata = trigger('routeAnimations', [
transition('HomePage => AddPage', [ transition('HomePage => AddPage', [
style({ transform: 'translateX(100%)', opacity: 0.5 }), style({ transform: 'translateX(100%)', opacity: 0.5 }),

View File

@@ -65,6 +65,14 @@ const routes: Routes = [
roles: ['org.read'], roles: ['org.read'],
}, },
}, },
{
path: 'grants',
loadChildren: () => import('./pages/grants/grants.module').then(m => m.GrantsModule),
canActivate: [AuthGuard, RoleGuard],
data: {
roles: ['user.grant.read'],
},
},
{ {
path: 'grant-create', path: 'grant-create',
canActivate: [AuthGuard], canActivate: [AuthGuard],
@@ -87,6 +95,24 @@ const routes: Routes = [
roles: ['user.grant.write'], roles: ['user.grant.write'],
}, },
}, },
{
path: 'user/:userid',
loadChildren: () => import('src/app/pages/user-grant-create/user-grant-create.module')
.then(m => m.UserGrantCreateModule),
canActivate: [RoleGuard],
data: {
roles: ['user.grant.write'],
},
},
{
path: '',
loadChildren: () => import('src/app/pages/user-grant-create/user-grant-create.module')
.then(m => m.UserGrantCreateModule),
canActivate: [RoleGuard],
data: {
roles: ['user.grant.write'],
},
},
], ],
}, },
{ {

View File

@@ -1,5 +1,5 @@
<ng-container *ngIf="(authService.user | async) || {} as user"> <ng-container *ngIf="(authService.user | async) || {} as user">
<ng-container *ngIf="((['iam.read','iam.write'] | hasRole)) as iamuser$"> <ng-container *ngIf="((['iam.read$','iam.write$'] | hasRole)) as iamuser$">
<mat-toolbar class="root-header"> <mat-toolbar class="root-header">
<button aria-label="Toggle sidenav" mat-icon-button (click)="drawer.toggle()"> <button aria-label="Toggle sidenav" mat-icon-button (click)="drawer.toggle()">
<i class="icon las la-bars"></i> <i class="icon las la-bars"></i>
@@ -12,22 +12,31 @@
</ng-template> </ng-template>
</a> </a>
<button (click)="loadOrgs()" *ngIf="profile?.id && org" mat-button <button (click)="loadOrgs()" *ngIf="profile?.id && org" mat-button [matMenuTriggerFor]="menu"
[matMenuTriggerFor]="menu">{{org?.name ? org.name : 'NO NAME'}} (menuOpened)="focusFilter()">{{org?.name ? org.name : 'NO NAME'}}
<mat-icon> <mat-icon>
arrow_drop_down</mat-icon> arrow_drop_down</mat-icon>
</button> </button>
<mat-menu #menu="matMenu"> <mat-menu class="menu" #menu="matMenu">
<mat-progress-bar *ngIf="orgLoading" color="accent" mode="indeterminate"></mat-progress-bar> <div class="spinner-w">
<button class="show-all" mat-menu-item <mat-spinner diameter="20" *ngIf="orgLoading$ | async" color="accent">
[routerLink]="[ '/org/overview' ]">{{'MENU.SHOWORGS' | translate}}</button> </mat-spinner>
</div>
<mat-form-field class="filter-form" appearance="fill">
<input matInput [formControl]="filterControl" autocomplete="off" (click)="$event.stopPropagation()"
placeholder="{{'ORG.PAGES.FILTERPLACEHOLDER' | translate}}" #input>
</mat-form-field>
<button [ngClass]="{'active': temporg.id === org?.id}" [disabled]="!temporg.id" <button [ngClass]="{'active': temporg.id === org?.id}" [disabled]="!temporg.id"
*ngFor="let temporg of orgs" mat-menu-item (click)="setActiveOrg(temporg)"> *ngFor="let temporg of orgs$ | async" mat-menu-item (click)="setActiveOrg(temporg)">
{{temporg?.name ? temporg.name : 'NO NAME'}} {{temporg?.name ? temporg.name : 'NO NAME'}}
</button> </button>
<button class="show-all" mat-menu-item
[routerLink]="[ '/org/overview' ]">{{'MENU.SHOWORGS' | translate}}</button>
<ng-template appHasRole [appHasRole]="['org.create','iam.write']"> <ng-template appHasRole [appHasRole]="['org.create','iam.write']">
<button mat-menu-item [routerLink]="[ '/org/create' ]"> <button mat-menu-item [routerLink]="[ '/org/create' ]">
<mat-icon class="avatar">add</mat-icon> <mat-icon class="avatar">add</mat-icon>
@@ -129,6 +138,21 @@
<span class="label">{{ 'MENU.MACHINEUSERS' | translate }}</span> <span class="label">{{ 'MENU.MACHINEUSERS' | translate }}</span>
</a> </a>
</ng-template> </ng-template>
<ng-template appHasRole [appHasRole]="['user.grant.read(:[0-9]*)?']">
<div @navitem class="divider">
<div class="line"></div>
<span class="label">
{{ 'MENU.GRANTSECTION' | translate }}</span>
<div class="line"></div>
</div>
<a @navitem class="nav-item" [routerLinkActive]="['active']" [routerLink]="[ '/grants']"
[routerLinkActiveOptions]="{ exact: true }">
<i class="icon las la-shield-alt"></i>
<span class="label">{{ 'MENU.GRANTS' | translate }}</span>
</a>
</ng-template>
</div> </div>
<span class="fill-space"></span> <span class="fill-space"></span>

View File

@@ -1,3 +1,4 @@
@import '~@angular/material/theming';
.root-header { .root-header {
position: fixed; position: fixed;
@@ -159,13 +160,6 @@
margin-bottom: 1rem; margin-bottom: 1rem;
} }
} }
.primary-button {
margin: 1rem;
border-radius: 1.5rem;
height: 2.5rem;
padding: 0 1rem;
}
} }
.content { .content {
@@ -174,7 +168,7 @@
.router { .router {
height: 100%; height: 100%;
overflow: auto; overflow-y: auto;
} }
} }
@@ -214,18 +208,50 @@
margin: .5rem 0; margin: .5rem 0;
span { span {
border: 1px solid #ffffff10; border: 1px solid #81868a40;
padding: 2px 1rem; padding: 2px 1rem;
border-radius: 50vw; border-radius: 50vw;
color: #8795a1; color: var(--grey);
font-size: 12px; font-size: 11px;
} }
.line { .line {
display: block; display: block;
background-color: #ffffff10; background-color: #81868a40;
height: 1px; height: 1px;
margin: .5rem 0; margin: .5rem 0;
flex: 1; flex: 1;
} }
} }
@mixin textvar($theme) {
.filter-form {
margin: 0 .5rem;
/* stylelint-disable */
$foreground: map-get($theme, foreground);
color: mat-color($foreground, text) !important;
}
.show-all {
$primary: map-get($theme, primary);
color: mat-color($primary, 300) !important;
border-bottom: 2px solid var(--grey);
margin-bottom: .5rem;
}
/* stylelint-enable */
}
.menu {
position: relative;
.spinner-w {
top: 1rem;
left: 0;
right: 0;
position: absolute;
display: flex;
justify-content: center;
align-items: center;
}
}

View File

@@ -1,17 +1,24 @@
import { BreakpointObserver } from '@angular/cdk/layout'; import { BreakpointObserver } from '@angular/cdk/layout';
import { OverlayContainer } from '@angular/cdk/overlay'; import { OverlayContainer } from '@angular/cdk/overlay';
import { ViewportScroller } from '@angular/common'; import { DOCUMENT, ViewportScroller } from '@angular/common';
import { Component, HostBinding, Inject, OnDestroy, ViewChild } from '@angular/core'; import { Component, ElementRef, HostBinding, Inject, OnDestroy, ViewChild } from '@angular/core';
import { FormControl } from '@angular/forms';
import { MatIconRegistry } from '@angular/material/icon'; import { MatIconRegistry } from '@angular/material/icon';
import { MatDrawer } from '@angular/material/sidenav'; import { MatDrawer } from '@angular/material/sidenav';
import { DomSanitizer } from '@angular/platform-browser'; import { DomSanitizer } from '@angular/platform-browser';
import { Router, RouterOutlet } from '@angular/router'; import { Router, RouterOutlet } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { LangChangeEvent, TranslateService } from '@ngx-translate/core';
import { Observable, of, Subscription } from 'rxjs'; import { BehaviorSubject, from, Observable, of, Subscription } from 'rxjs';
import { map } from 'rxjs/operators'; import { catchError, debounceTime, finalize, map, take } from 'rxjs/operators';
import { accountCard, navAnimations, routeAnimations, toolbarAnimation } from './animations'; import { accountCard, navAnimations, routeAnimations, toolbarAnimation } from './animations';
import { Org, UserProfileView } from './proto/generated/auth_pb'; import {
MyProjectOrgSearchKey,
MyProjectOrgSearchQuery,
Org,
SearchMethod,
UserProfileView,
} from './proto/generated/auth_pb';
import { AuthenticationService } from './services/authentication.service'; import { AuthenticationService } from './services/authentication.service';
import { GrpcAuthService } from './services/grpc-auth.service'; import { GrpcAuthService } from './services/grpc-auth.service';
import { ManagementService } from './services/mgmt.service'; import { ManagementService } from './services/mgmt.service';
@@ -32,6 +39,7 @@ import { UpdateService } from './services/update.service';
}) })
export class AppComponent implements OnDestroy { export class AppComponent implements OnDestroy {
@ViewChild('drawer') public drawer!: MatDrawer; @ViewChild('drawer') public drawer!: MatDrawer;
@ViewChild('input', { static: false }) input!: ElementRef;
public isHandset$: Observable<boolean> = this.breakpointObserver public isHandset$: Observable<boolean> = this.breakpointObserver
.observe('(max-width: 599px)') .observe('(max-width: 599px)')
.pipe(map(result => { .pipe(map(result => {
@@ -41,17 +49,18 @@ export class AppComponent implements OnDestroy {
public showAccount: boolean = false; public showAccount: boolean = false;
public org!: Org.AsObject; public org!: Org.AsObject;
public orgs: Org.AsObject[] = []; public orgs$: Observable<Org.AsObject[]> = of([]);
public profile!: UserProfileView.AsObject; public profile!: UserProfileView.AsObject;
public isDarkTheme: Observable<boolean> = of(true); public isDarkTheme: Observable<boolean> = of(true);
public orgLoading: boolean = false; public orgLoading$: BehaviorSubject<any> = new BehaviorSubject(false);
public showProjectSection: boolean = false; public showProjectSection: boolean = false;
public grantedProjectsCount: number = 0; public grantedProjectsCount: number = 0;
public ownedProjectsCount: number = 0; public ownedProjectsCount: number = 0;
public filterControl: FormControl = new FormControl('');
private authSub: Subscription = new Subscription(); private authSub: Subscription = new Subscription();
private orgSub: Subscription = new Subscription(); private orgSub: Subscription = new Subscription();
@@ -70,6 +79,7 @@ export class AppComponent implements OnDestroy {
private toast: ToastService, private toast: ToastService,
private router: Router, private router: Router,
update: UpdateService, update: UpdateService,
@Inject(DOCUMENT) private document: Document,
) { ) {
console.log('%cWait!', 'text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black; color: #5282c1; font-size: 50px'); console.log('%cWait!', 'text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black; color: #5282c1; font-size: 50px');
console.log('%cInserting something here could give attackers access to your zitadel account.', 'color: red; font-size: 18px'); console.log('%cInserting something here could give attackers access to your zitadel account.', 'color: red; font-size: 18px');
@@ -140,7 +150,6 @@ export class AppComponent implements OnDestroy {
this.orgSub = this.authService.activeOrgChanged.subscribe(org => { this.orgSub = this.authService.activeOrgChanged.subscribe(org => {
this.org = org; this.org = org;
this.getProjectCount(); this.getProjectCount();
}); });
@@ -160,6 +169,16 @@ export class AppComponent implements OnDestroy {
this.isDarkTheme = this.themeService.isDarkTheme; this.isDarkTheme = this.themeService.isDarkTheme;
this.isDarkTheme.subscribe(thema => this.onSetTheme(thema ? 'dark-theme' : 'light-theme')); this.isDarkTheme.subscribe(thema => this.onSetTheme(thema ? 'dark-theme' : 'light-theme'));
this.translate.onLangChange.subscribe((language: LangChangeEvent) => {
this.document.documentElement.lang = language.lang;
});
this.filterControl.valueChanges.pipe(debounceTime(300)).subscribe(value => {
this.loadOrgs(
value.trim().toLowerCase(),
);
});
} }
public ngOnDestroy(): void { public ngOnDestroy(): void {
@@ -167,15 +186,26 @@ export class AppComponent implements OnDestroy {
this.orgSub.unsubscribe(); this.orgSub.unsubscribe();
} }
public loadOrgs(): void { public loadOrgs(filter?: string): void {
this.orgLoading = true; let query;
this.authService.SearchMyProjectOrgs(10, 0).then(res => { if (filter) {
this.orgs = res.toObject().resultList; query = new MyProjectOrgSearchQuery();
this.orgLoading = false; query.setMethod(SearchMethod.SEARCHMETHOD_CONTAINS_IGNORE_CASE);
}).catch(error => { query.setKey(MyProjectOrgSearchKey.MYPROJECTORGSEARCHKEY_ORG_NAME);
this.toast.showError(error); query.setValue(filter);
this.orgLoading = false; }
});
this.orgLoading$.next(true);
this.orgs$ = from(this.authService.SearchMyProjectOrgs(10, 0, query ? [query] : undefined)).pipe(
map(resp => {
return resp.toObject().resultList;
}),
catchError(() => of([])),
finalize(() => {
this.orgLoading$.next(false);
this.focusFilter();
}),
);
} }
public prepareRoute(outlet: RouterOutlet): boolean { public prepareRoute(outlet: RouterOutlet): boolean {
@@ -200,19 +230,24 @@ export class AppComponent implements OnDestroy {
this.authService.user.subscribe(userprofile => { this.authService.user.subscribe(userprofile => {
this.profile = userprofile; this.profile = userprofile;
const lang = userprofile.preferredLanguage.match(/en|de/) ? userprofile.preferredLanguage : 'en'; const cropped = navigator.language.split('-')[0] ?? 'en';
const fallbackLang = cropped.match(/en|de/) ? cropped : 'en';
const lang = userprofile.preferredLanguage.match(/en|de/) ? userprofile.preferredLanguage : fallbackLang;
this.translate.use(lang); this.translate.use(lang);
this.document.documentElement.lang = lang;
}); });
} }
public setActiveOrg(org: Org.AsObject): void { public setActiveOrg(org: Org.AsObject): void {
this.org = org; this.org = org;
this.authService.setActiveOrg(org); this.authService.setActiveOrg(org);
this.router.navigate(['/']); this.authService.zitadelPermissionsChanged.pipe(take(1)).subscribe(() => {
this.router.navigate(['/']);
});
} }
private getProjectCount(): void { private getProjectCount(): void {
this.authService.isAllowed(['project.read']).subscribe((allowed) => { this.authService.isAllowed(['project.read$']).subscribe((allowed) => {
if (allowed) { if (allowed) {
this.mgmtService.SearchProjects(0, 0).then(res => { this.mgmtService.SearchProjects(0, 0).then(res => {
this.ownedProjectsCount = res.toObject().totalResult; this.ownedProjectsCount = res.toObject().totalResult;
@@ -224,5 +259,11 @@ export class AppComponent implements OnDestroy {
} }
}); });
} }
focusFilter(): void {
setTimeout(() => {
this.input.nativeElement.focus();
}, 0);
}
} }

View File

@@ -3,13 +3,17 @@ import { CommonModule, registerLocaleData } from '@angular/common';
import { HttpClient, HttpClientModule } from '@angular/common/http'; import { HttpClient, HttpClientModule } from '@angular/common/http';
import localeDe from '@angular/common/locales/de'; import localeDe from '@angular/common/locales/de';
import { APP_INITIALIZER, NgModule } from '@angular/core'; import { APP_INITIALIZER, NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
import { MatNativeDateModule } from '@angular/material/core'; import { MatNativeDateModule } from '@angular/material/core';
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu';
import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSidenavModule } from '@angular/material/sidenav'; import { MatSidenavModule } from '@angular/material/sidenav';
import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatToolbarModule } from '@angular/material/toolbar'; import { MatToolbarModule } from '@angular/material/toolbar';
@@ -21,7 +25,7 @@ import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { AuthConfig, OAuthModule, OAuthStorage } from 'angular-oauth2-oidc'; import { AuthConfig, OAuthModule, OAuthStorage } from 'angular-oauth2-oidc';
import { QuicklinkModule } from 'ngx-quicklink'; import { QuicklinkModule } from 'ngx-quicklink';
import { RegExpPipeModule } from 'src/app/pipes/regexp-pipe.module'; import { RegExpPipeModule } from 'src/app/pipes/regexp-pipe/regexp-pipe.module';
import { environment } from '../environments/environment'; import { environment } from '../environments/environment';
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from './app-routing.module';
@@ -32,13 +36,15 @@ import { AccountsCardModule } from './modules/accounts-card/accounts-card.module
import { AvatarModule } from './modules/avatar/avatar.module'; import { AvatarModule } from './modules/avatar/avatar.module';
import { WarnDialogModule } from './modules/warn-dialog/warn-dialog.module'; import { WarnDialogModule } from './modules/warn-dialog/warn-dialog.module';
import { SignedoutComponent } from './pages/signedout/signedout.component'; import { SignedoutComponent } from './pages/signedout/signedout.component';
import { HasRolePipeModule } from './pipes/has-role-pipe.module'; import { HasRolePipeModule } from './pipes/has-role-pipe/has-role-pipe.module';
import { GrpcAuthService } from './services/grpc-auth.service'; import { GrpcAuthService } from './services/grpc-auth.service';
import { GrpcService } from './services/grpc.service'; import { GrpcService } from './services/grpc.service';
import { AuthInterceptor } from './services/interceptors/auth.interceptor'; import { AuthInterceptor } from './services/interceptors/auth.interceptor';
import { GRPC_INTERCEPTORS } from './services/interceptors/grpc-interceptor'; import { GRPC_INTERCEPTORS } from './services/interceptors/grpc-interceptor';
import { I18nInterceptor } from './services/interceptors/i18n.interceptor';
import { OrgInterceptor } from './services/interceptors/org.interceptor'; import { OrgInterceptor } from './services/interceptors/org.interceptor';
import { RefreshService } from './services/refresh.service'; import { RefreshService } from './services/refresh.service';
import { SeoService } from './services/seo.service';
import { StatehandlerProcessorService, StatehandlerProcessorServiceImpl } from './services/statehandler-processor.service'; import { StatehandlerProcessorService, StatehandlerProcessorServiceImpl } from './services/statehandler-processor.service';
import { StatehandlerService, StatehandlerServiceImpl } from './services/statehandler.service'; import { StatehandlerService, StatehandlerServiceImpl } from './services/statehandler.service';
import { StorageService } from './services/storage.service'; import { StorageService } from './services/storage.service';
@@ -104,9 +110,13 @@ const authConfig: AuthConfig = {
MatSidenavModule, MatSidenavModule,
MatCardModule, MatCardModule,
OutsideClickModule, OutsideClickModule,
MatFormFieldModule,
MatInputModule,
HasRolePipeModule, HasRolePipeModule,
MatProgressBarModule, MatProgressBarModule,
MatProgressSpinnerModule,
MatToolbarModule, MatToolbarModule,
ReactiveFormsModule,
MatMenuModule, MatMenuModule,
MatSnackBarModule, MatSnackBarModule,
AvatarModule, AvatarModule,
@@ -150,11 +160,17 @@ const authConfig: AuthConfig = {
multi: true, multi: true,
useClass: AuthInterceptor, useClass: AuthInterceptor,
}, },
{
provide: GRPC_INTERCEPTORS,
multi: true,
useClass: I18nInterceptor,
},
{ {
provide: GRPC_INTERCEPTORS, provide: GRPC_INTERCEPTORS,
multi: true, multi: true,
useClass: OrgInterceptor, useClass: OrgInterceptor,
}, },
SeoService,
RefreshService, RefreshService,
GrpcService, GrpcService,
GrpcAuthService, GrpcAuthService,

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router'; import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { filter, first, switchMap } from 'rxjs/operators';
import { GrpcAuthService } from '../services/grpc-auth.service'; import { GrpcAuthService } from '../services/grpc-auth.service';
@@ -15,6 +16,11 @@ export class RoleGuard implements CanActivate {
route: ActivatedRouteSnapshot, route: ActivatedRouteSnapshot,
state: RouterStateSnapshot, state: RouterStateSnapshot,
): Observable<boolean> { ): Observable<boolean> {
return this.authService.isAllowed(route.data['roles']); return this.authService.fetchedZitadelPermissions.pipe(
filter((permissionsFetched) => !!permissionsFetched),
first(),
).pipe(
switchMap(_ => this.authService.isAllowed(route.data['roles'])),
);
} }
} }

View File

@@ -0,0 +1,28 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';
import { GrpcAuthService } from '../services/grpc-auth.service';
@Injectable({
providedIn: 'root',
})
export class UserGuard implements CanActivate {
constructor(private authService: GrpcAuthService, private router: Router) { }
public canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
): Observable<boolean> | Promise<boolean> | boolean {
return this.authService.user.pipe(
map(user => user.id !== route.params.id),
tap((isNotMe) => {
if (!isNotMe) {
this.router.navigate(['/users', 'me']);
}
}),
);
}
}

View File

@@ -16,7 +16,7 @@
</app-avatar> </app-avatar>
<div class="col"> <div class="col">
<span class="title">{{user.displayName ? user.displayName : user.userName}} </span> <span class="user-title">{{user.displayName ? user.displayName : user.userName}} </span>
<span class="loginname">{{user.loginName}}</span> <span class="loginname">{{user.loginName}}</span>
<span class="email">{{'USER.STATE.'+user.authState | translate}}</span> <span class="email">{{'USER.STATE.'+user.authState | translate}}</span>
</div> </div>
@@ -28,7 +28,7 @@
<i class="las la-user-plus"></i> <i class="las la-user-plus"></i>
</div> </div>
<span class="col"> <span class="col">
<span class="title">{{'USER.ADDACCOUNT' | translate}}</span> <span class="user-title">{{'USER.ADDACCOUNT' | translate}}</span>
</span> </span>
<span class="fill-space"></span> <span class="fill-space"></span>
<mat-icon>keyboard_arrow_right</mat-icon> <mat-icon>keyboard_arrow_right</mat-icon>

View File

@@ -41,9 +41,11 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
border-top: 1px solid #ffffff30;
border-bottom: 1px solid #ffffff30;
padding: .5rem 0; padding: .5rem 0;
max-height: 310px;
overflow-y: auto;
border-top: 1px solid rgba(#8795a1, .3);
border-bottom: 1px solid rgba(#8795a1, .3);
.row { .row {
padding: .5rem; padding: .5rem;
@@ -84,7 +86,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.title { .user-title {
font-weight: 500; font-weight: 500;
font-size: .9rem; font-size: .9rem;
line-height: 1rem; line-height: 1rem;
@@ -92,7 +94,7 @@
.email, .email,
.loginname { .loginname {
color: #8795a1; color: var(--grey);
font-size: .8rem; font-size: .8rem;
line-height: 1rem; line-height: 1rem;
} }

View File

@@ -21,7 +21,9 @@ export class AccountsCardComponent implements OnInit {
this.userService.getMyUserSessions().then(sessions => { this.userService.getMyUserSessions().then(sessions => {
this.users = sessions.toObject().userSessionsList; this.users = sessions.toObject().userSessionsList;
const index = this.users.findIndex(user => user.loginName === this.profile.preferredLoginName); const index = this.users.findIndex(user => user.loginName === this.profile.preferredLoginName);
this.users.splice(index, 1); if (index > -1) {
this.users.splice(index, 1);
}
this.loadingUsers = false; this.loadingUsers = false;
}).catch(() => { }).catch(() => {

View File

@@ -9,8 +9,9 @@
<mat-form-field class="full-width" appearance="outline"> <mat-form-field class="full-width" appearance="outline">
<mat-label>{{ 'MEMBER.CREATIONTYPE' | translate }}</mat-label> <mat-label>{{ 'MEMBER.CREATIONTYPE' | translate }}</mat-label>
<mat-select [(ngModel)]="creationType" (selectionChange)="loadRoles()"> <mat-select [(ngModel)]="creationType" (selectionChange)="loadRoles()">
<mat-option *ngFor="let type of creationTypes" [value]="type"> <mat-option *ngFor="let type of creationTypes" [value]="type.type"
{{ 'MEMBER.CREATIONTYPES.'+type | translate}} [disabled]="(type.disabled$ | async) == false">
{{ 'MEMBER.CREATIONTYPES.'+type.type | translate}}
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>

View File

@@ -3,7 +3,7 @@
} }
.desc { .desc {
color: #8795a1; color: var(--grey);
font-size: .9rem; font-size: .9rem;
} }
@@ -18,8 +18,4 @@
.ok-button { .ok-button {
margin-left: .5rem; margin-left: .5rem;
} }
button {
border-radius: .5rem;
}
} }

View File

@@ -1,7 +1,9 @@
import { Component, Inject } from '@angular/core'; import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Observable } from 'rxjs';
import { ProjectGrantView, ProjectRole, ProjectView, UserView } from 'src/app/proto/generated/management_pb'; import { ProjectGrantView, ProjectRole, ProjectView, UserView } from 'src/app/proto/generated/management_pb';
import { AdminService } from 'src/app/services/admin.service'; import { AdminService } from 'src/app/services/admin.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ManagementService } from 'src/app/services/mgmt.service'; import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
@@ -23,13 +25,18 @@ export class MemberCreateDialogComponent {
private grantId: string = ''; private grantId: string = '';
public preselectedUsers: Array<UserView.AsObject> = []; public preselectedUsers: Array<UserView.AsObject> = [];
public creationType!: CreationType; public creationType!: CreationType;
public creationTypes: CreationType[] = [
CreationType.IAM, /**
CreationType.ORG, * Specifies options for creating members,
CreationType.PROJECT_OWNED, * without ending $, to enable write event permission even if user is allowed
CreationType.PROJECT_GRANTED, * to create members for only one specific project.
*/
public creationTypes: Array<{ type: CreationType, disabled$: Observable<boolean>; }> = [
{ type: CreationType.IAM, disabled$: this.authService.isAllowed(['iam.member.write$']) },
{ type: CreationType.ORG, disabled$: this.authService.isAllowed(['org.member.write$']) },
{ type: CreationType.PROJECT_OWNED, disabled$: this.authService.isAllowed(['project.member.write']) },
{ type: CreationType.PROJECT_GRANTED, disabled$: this.authService.isAllowed(['project.grant.member.write']) },
]; ];
public users: Array<UserView.AsObject> = []; public users: Array<UserView.AsObject> = [];
public roles: Array<ProjectRole.AsObject> | string[] = []; public roles: Array<ProjectRole.AsObject> | string[] = [];
@@ -41,6 +48,7 @@ export class MemberCreateDialogComponent {
constructor( constructor(
private mgmtService: ManagementService, private mgmtService: ManagementService,
private adminService: AdminService, private adminService: AdminService,
private authService: GrpcAuthService,
public dialogRef: MatDialogRef<MemberCreateDialogComponent>, public dialogRef: MatDialogRef<MemberCreateDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any, @Inject(MAT_DIALOG_DATA) public data: any,
private toastService: ToastService, private toastService: ToastService,

View File

@@ -1,9 +1,11 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatChipsModule } from '@angular/material/chips';
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { SearchUserAutocompleteModule } from 'src/app/modules/search-user-autocomplete/search-user-autocomplete.module'; import { SearchUserAutocompleteModule } from 'src/app/modules/search-user-autocomplete/search-user-autocomplete.module';
@@ -21,10 +23,13 @@ import { MemberCreateDialogComponent } from './member-create-dialog.component';
CommonModule, CommonModule,
MatDialogModule, MatDialogModule,
MatButtonModule, MatButtonModule,
MatChipsModule,
MatInputModule,
TranslateModule, TranslateModule,
MatFormFieldModule, MatFormFieldModule,
MatSelectModule, MatSelectModule,
FormsModule, FormsModule,
ReactiveFormsModule,
SearchUserAutocompleteModule, SearchUserAutocompleteModule,
SearchRolesAutocompleteModule, SearchRolesAutocompleteModule,
SearchProjectAutocompleteModule, SearchProjectAutocompleteModule,

View File

@@ -34,7 +34,7 @@
.desc { .desc {
font-size: .9rem; font-size: .9rem;
color: #8795a1; color: var(--grey);
} }
} }

View File

@@ -11,7 +11,7 @@
.card { .card {
background-color: $primary-dark; background-color: $primary-dark;
transition: background-color .4s ease-in-out; transition: background-color .3s cubic-bezier(.645, .045, .355, 1);
border: 1px solid rgba($border-color, .2); border: 1px solid rgba($border-color, .2);
box-sizing: border-box; box-sizing: border-box;
border-radius: .5rem; border-radius: .5rem;

View File

@@ -21,13 +21,13 @@
flex-direction: column; flex-direction: column;
.editor { .editor {
color: #8795a1; color: var(--grey);
font-size: 12px; font-size: 12px;
align-self: flex-end; align-self: flex-end;
} }
.seq { .seq {
color: #8795a1; color: var(--grey);
font-size: 12px; font-size: 12px;
align-self: flex-end; align-self: flex-end;
} }
@@ -43,7 +43,7 @@
&.change-item-back { &.change-item-back {
background-color: rgba($primary-dark, .93); background-color: rgba($primary-dark, .93);
transition: background-color .4s ease-in-out; transition: background-color .3s cubic-bezier(.645, .045, .355, 1);
} }
} }
@@ -55,7 +55,7 @@
.end-container { .end-container {
font-size: 12px; font-size: 12px;
color: #8795a1; color: var(--grey);
} }
} }
} }

View File

@@ -103,11 +103,9 @@ export class ChangesComponent implements OnInit {
this._loading.next(true); this._loading.next(true);
return from(col).pipe( return from(col).pipe(
take(1),
tap((res: Changes) => { tap((res: Changes) => {
let values = res.toObject().changesList; const values = res.toObject().changesList;
// If prepending, reverse the batch order
values = false ? values.reverse() : values;
// update source with new values, done loading // update source with new values, done loading
this._data.next(values); this._data.next(values);
@@ -118,12 +116,11 @@ export class ChangesComponent implements OnInit {
this._done.next(true); this._done.next(true);
} }
}), }),
catchError(err => { catchError(_ => {
this._loading.next(false); this._loading.next(false);
this.bottom = true; this.bottom = true;
return of([]); return of([]);
}), }),
take(1),
).subscribe(); ).subscribe();
} }
} }

View File

@@ -4,9 +4,9 @@ import { NgModule } from '@angular/core';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { ScrollableModule } from 'src/app/directives/scrollable/scrollable.module'; import { ScrollableModule } from 'src/app/directives/scrollable/scrollable.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe.module'; import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe.module'; import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module';
import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe.module'; import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module';
import { ChangesComponent } from './changes.component'; import { ChangesComponent } from './changes.component';

View File

@@ -8,13 +8,19 @@
<ng-container *ngIf="totalResult < 10; else compact"> <ng-container *ngIf="totalResult < 10; else compact">
<ng-container *ngFor="let member of membersSubject | async; index as i"> <ng-container *ngFor="let member of membersSubject | async; index as i">
<div @animate (click)="emitShowDetail()" class="avatar-circle" <div @animate (click)="emitShowDetail()" class="avatar-circle"
matTooltip="{{ member.email }} | {{member.rolesList?.join(' ')}}" matTooltip="{{ member.displayName }} | {{member.rolesList?.join(' ')}}"
[ngStyle]="{'z-index': 100 - i}"> [ngStyle]="{'z-index': 100 - i}">
<app-avatar *ngIf="member && (member.displayName || (member.firstName && member.lastName))" <app-avatar
*ngIf="member && member.displayName && member.firstName && member.lastName; else cog"
class="avatar dontcloseonclick" class="avatar dontcloseonclick"
[name]="member.displayName ? member.displayName : (member.firstName + ' '+ member.lastName)" [name]="member.displayName ? member.displayName : (member.firstName + ' '+ member.lastName)"
[size]="32"> [size]="32">
</app-avatar> </app-avatar>
<ng-template #cog>
<div class="sa-icon">
<i class="las la-user-cog"></i>
</div>
</ng-template>
</div> </div>
</ng-container> </ng-container>
</ng-container> </ng-container>

View File

@@ -9,7 +9,7 @@
.sub-header { .sub-header {
font-size: .8rem; font-size: .8rem;
color: #8795a1; color: var(--grey);
} }
.people { .people {
@@ -65,6 +65,17 @@
.avatar { .avatar {
pointer-events: none; pointer-events: none;
} }
.sa-icon {
display: block;
width: 32px;
margin: 0 .5rem;
i {
margin: auto;
font-size: 1.2rem;
}
}
} }
.margin-neg { .margin-neg {

View File

@@ -1,14 +1,16 @@
<div class="max-width-container detail-container"> <div class="max-width-container">
<div class="detail-left"> <div class="detail-container">
<a *ngIf="backRouterLink" [routerLink]="backRouterLink" mat-icon-button> <div class="detail-left">
<mat-icon class="icon">arrow_back</mat-icon> <a *ngIf="backRouterLink" [routerLink]="backRouterLink" mat-icon-button>
</a> <mat-icon class="icon">arrow_back</mat-icon>
</div> </a>
<div class="detail-right"> </div>
<div class="head"> <div class="detail-right">
<h1>{{ title }}</h1> <div class="head">
<p class="desc">{{ description }}</p> <h1>{{ title }}</h1>
<ng-content></ng-content> <p class="desc">{{ description }}</p>
<ng-content></ng-content>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -10,9 +10,9 @@
$lighter-color: rgba(mat-color($primary, 300), .5); $lighter-color: rgba(mat-color($primary, 300), .5);
.detail-container { .detail-container {
width: 100%;
display: flex; display: flex;
padding-bottom: 3rem; padding-bottom: 3rem;
padding-top: 3rem;
.detail-left { .detail-left {
width: 100px; width: 100px;
@@ -29,7 +29,6 @@
.detail-right { .detail-right {
flex: 1; flex: 1;
position: relative;
padding-left: 1rem; padding-left: 1rem;
@media only screen and (max-width: 500px) { @media only screen and (max-width: 500px) {
@@ -37,20 +36,17 @@
} }
.head { .head {
display: flex;
align-items: center;
margin-bottom: 2rem; margin-bottom: 2rem;
flex-wrap: wrap;
h1 { h1 {
font-size: 1.2rem; font-size: 1.5rem;
margin-top: 10px;
} }
.desc { .desc {
width: 100%;
display: block; display: block;
font-size: .9rem; font-size: .9rem;
color: #8795a1; color: var(--grey);
} }
} }
} }

View File

@@ -11,6 +11,8 @@
<h1>{{'IDP.CREATE.TITLE' | translate}}</h1> <h1>{{'IDP.CREATE.TITLE' | translate}}</h1>
<p>{{'IDP.CREATE.DESCRIPTION' | translate}}</p> <p>{{'IDP.CREATE.DESCRIPTION' | translate}}</p>
<mat-progress-bar *ngIf="loading" color="primary" mode="indeterminate"></mat-progress-bar>
<form (ngSubmit)="addIdp()"> <form (ngSubmit)="addIdp()">
<ng-container [formGroup]="formGroup"> <ng-container [formGroup]="formGroup">
<div class="content"> <div class="content">
@@ -48,22 +50,22 @@
</mat-form-field> </mat-form-field>
</div> </div>
<div class="content"> <div class="content">
<mat-form-field class="formfield" appearance="outline"> <mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'IDP.IDPDISPLAYNAMMAPPING' | translate }}</mat-label> <mat-label>{{ 'IDP.IDPDISPLAYNAMMAPPING' | translate }}</mat-label>
<mat-select formControlName="idpDisplayNameMapping"> <mat-select formControlName="idpDisplayNameMapping">
<mat-option *ngFor="let field of mappingFields" [value]="field"> <mat-option *ngFor="let field of mappingFields" [value]="field">
{{ 'IDP.MAPPINTFIELD.'+field | translate }} {{ 'IDP.MAPPINTFIELD.'+field | translate }}
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field class="formfield" appearance="outline"> <mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'IDP.USERNAMEMAPPING' | translate }}</mat-label> <mat-label>{{ 'IDP.USERNAMEMAPPING' | translate }}</mat-label>
<mat-select formControlName="usernameMapping"> <mat-select formControlName="usernameMapping">
<mat-option *ngFor="let field of mappingFields" [value]="field"> <mat-option *ngFor="let field of mappingFields" [value]="field">
{{ 'IDP.MAPPINTFIELD.'+field | translate }} {{ 'IDP.MAPPINTFIELD.'+field | translate }}
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
</ng-container> </ng-container>
@@ -71,4 +73,4 @@
{{ 'ACTIONS.SAVE' | translate }} {{ 'ACTIONS.SAVE' | translate }}
</button> </button>
</form> </form>
</div> </div>

View File

@@ -25,7 +25,6 @@
.add-line-btn { .add-line-btn {
margin-bottom: 1rem; margin-bottom: 1rem;
border-radius: .5rem;
} }
} }
@@ -38,7 +37,7 @@
flex-basis: 100%; flex-basis: 100%;
margin: 0 .5rem; margin: 0 .5rem;
margin-bottom: 1rem; margin-bottom: 1rem;
color: #8795a1; color: var(--grey);
} }
.formfield { .formfield {
@@ -55,7 +54,6 @@
margin-top: 3rem; margin-top: 3rem;
display: block; display: block;
padding: .5rem 4rem; padding: .5rem 4rem;
border-radius: .5rem;
@media only screen and (max-width: 450px) { @media only screen and (max-width: 450px) {
margin-top: 1rem; margin-top: 1rem;

View File

@@ -7,18 +7,18 @@ import { ActivatedRoute, Params, Router } from '@angular/router';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
import { import {
OidcIdpConfigCreate as AdminOidcIdpConfigCreate, OidcIdpConfigCreate as AdminOidcIdpConfigCreate,
OIDCMappingField as authMappingFields, OIDCMappingField as authMappingFields,
} from 'src/app/proto/generated/admin_pb'; } from 'src/app/proto/generated/admin_pb';
import { AdminService } from 'src/app/services/admin.service'; import { AdminService } from 'src/app/services/admin.service';
import { ManagementService } from 'src/app/services/mgmt.service'; import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
import { PolicyComponentServiceType } from '../policies/policy-component-types.enum';
import { import {
OidcIdpConfigCreate as MgmtOidcIdpConfigCreate, OidcIdpConfigCreate as MgmtOidcIdpConfigCreate,
OIDCMappingField as mgmtMappingFields, OIDCMappingField as mgmtMappingFields,
} from '../../proto/generated/management_pb'; } from '../../proto/generated/management_pb';
import { PolicyComponentServiceType } from '../policies/policy-component-types.enum';
@Component({ @Component({
selector: 'app-idp-create', selector: 'app-idp-create',
@@ -37,7 +37,7 @@ export class IdpCreateComponent implements OnInit, OnDestroy {
public formGroup!: FormGroup; public formGroup!: FormGroup;
public createSteps: number = 1; public createSteps: number = 1;
public currentCreateStep: number = 1; public currentCreateStep: number = 1;
public loading: boolean = false;
constructor( constructor(
private router: Router, private router: Router,
private route: ActivatedRoute, private route: ActivatedRoute,
@@ -47,7 +47,6 @@ export class IdpCreateComponent implements OnInit, OnDestroy {
) { ) {
this.formGroup = new FormGroup({ this.formGroup = new FormGroup({
name: new FormControl('', [Validators.required]), name: new FormControl('', [Validators.required]),
logoSrc: new FormControl('', []),
clientId: new FormControl('', [Validators.required]), clientId: new FormControl('', [Validators.required]),
clientSecret: new FormControl('', [Validators.required]), clientSecret: new FormControl('', [Validators.required]),
issuer: new FormControl('', [Validators.required]), issuer: new FormControl('', [Validators.required]),
@@ -68,8 +67,8 @@ export class IdpCreateComponent implements OnInit, OnDestroy {
case PolicyComponentServiceType.ADMIN: case PolicyComponentServiceType.ADMIN:
this.service = this.injector.get(AdminService as Type<AdminService>); this.service = this.injector.get(AdminService as Type<AdminService>);
this.mappingFields = [ this.mappingFields = [
authMappingFields.OIDCMAPPINGFIELD_PREFERRED_USERNAME, authMappingFields.OIDCMAPPINGFIELD_PREFERRED_USERNAME,
authMappingFields.OIDCMAPPINGFIELD_EMAIL]; authMappingFields.OIDCMAPPINGFIELD_EMAIL];
break; break;
} }
}); });
@@ -91,25 +90,30 @@ export class IdpCreateComponent implements OnInit, OnDestroy {
let req: AdminOidcIdpConfigCreate | MgmtOidcIdpConfigCreate; let req: AdminOidcIdpConfigCreate | MgmtOidcIdpConfigCreate;
switch (this.serviceType) { switch (this.serviceType) {
case PolicyComponentServiceType.MGMT: case PolicyComponentServiceType.MGMT:
req = new MgmtOidcIdpConfigCreate(); req = new MgmtOidcIdpConfigCreate();
break; break;
case PolicyComponentServiceType.ADMIN: case PolicyComponentServiceType.ADMIN:
req = new AdminOidcIdpConfigCreate(); req = new AdminOidcIdpConfigCreate();
break; break;
} }
req.setName(this.name?.value); req.setName(this.name?.value);
req.setClientId(this.clientId?.value); req.setClientId(this.clientId?.value);
req.setClientSecret(this.clientSecret?.value); req.setClientSecret(this.clientSecret?.value);
req.setIssuer(this.issuer?.value); req.setIssuer(this.issuer?.value);
req.setLogoSrc(this.logoSrc?.value);
req.setScopesList(this.scopesList?.value); req.setScopesList(this.scopesList?.value);
req.setIdpDisplayNameMapping(this.idpDisplayNameMapping?.value); req.setIdpDisplayNameMapping(this.idpDisplayNameMapping?.value);
req.setUsernameMapping(this.usernameMapping?.value); req.setUsernameMapping(this.usernameMapping?.value);
this.loading = true;
this.service.CreateOidcIdp(req).then((idp) => { this.service.CreateOidcIdp(req).then((idp) => {
this.router.navigate(['idp', idp.getId()]); setTimeout(() => {
this.loading = false;
this.router.navigate([
this.serviceType === PolicyComponentServiceType.MGMT ? 'org' :
this.serviceType === PolicyComponentServiceType.ADMIN ? 'iam' : '',
'idp', idp.getId()]);
}, 2000);
}).catch(error => { }).catch(error => {
this.toast.showError(error); this.toast.showError(error);
}); });
@@ -147,10 +151,6 @@ export class IdpCreateComponent implements OnInit, OnDestroy {
return this.formGroup.get('name'); return this.formGroup.get('name');
} }
public get logoSrc(): AbstractControl | null {
return this.formGroup.get('logoSrc');
}
public get clientId(): AbstractControl | null { public get clientId(): AbstractControl | null {
return this.formGroup.get('clientId'); return this.formGroup.get('clientId');
} }
@@ -163,7 +163,7 @@ export class IdpCreateComponent implements OnInit, OnDestroy {
return this.formGroup.get('issuer'); return this.formGroup.get('issuer');
} }
public get scopesList(): AbstractControl | null { public get scopesList(): AbstractControl | null {
return this.formGroup.get('scopesList'); return this.formGroup.get('scopesList');
} }
public get idpDisplayNameMapping(): AbstractControl | null { public get idpDisplayNameMapping(): AbstractControl | null {
@@ -171,7 +171,7 @@ export class IdpCreateComponent implements OnInit, OnDestroy {
} }
public get usernameMapping(): AbstractControl | null { public get usernameMapping(): AbstractControl | null {
return this.formGroup.get('usernameMapping'); return this.formGroup.get('usernameMapping');
} }
} }

View File

@@ -6,12 +6,13 @@ import { MatChipsModule } from '@angular/material/chips';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatSelectModule } from '@angular/material/select';
import { MatTooltipModule } from '@angular/material/tooltip'; import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { IdpCreateRoutingModule } from './idp-create-routing.module'; import { IdpCreateRoutingModule } from './idp-create-routing.module';
import { IdpCreateComponent } from './idp-create.component'; import { IdpCreateComponent } from './idp-create.component';
import {MatSelectModule} from '@angular/material/select';
@NgModule({ @NgModule({
declarations: [IdpCreateComponent], declarations: [IdpCreateComponent],
@@ -28,6 +29,7 @@ import {MatSelectModule} from '@angular/material/select';
MatChipsModule, MatChipsModule,
MatTooltipModule, MatTooltipModule,
TranslateModule, TranslateModule,
MatProgressBarModule,
], ],
}) })
export class IdpCreateModule { } export class IdpCreateModule { }

View File

@@ -1,19 +1,19 @@
<app-refresh-table [loading]="loading$ | async" (refreshed)="refreshPage()" [dataSize]="dataSource.data.length" <app-refresh-table [loading]="loading$ | async" (refreshed)="refreshPage()" [dataSize]="dataSource.data.length"
[timestamp]="idpResult?.viewTimestamp" [selection]="selection"> [emitRefreshOnPreviousRoutes]="['/iam/idp/create']" [timestamp]="idpResult?.viewTimestamp" [selection]="selection">
<ng-template appHasRole [appHasRole]="['iam.write']" actions> <ng-template appHasRole [appHasRole]="['iam.write']" actions>
<button (click)="deactivateSelectedIdps()" matTooltip="{{'IDP.DEACTIVATE' | translate}}" class="icon-button" <button (click)="deactivateSelectedIdps()" matTooltip="{{'IDP.DEACTIVATE' | translate}}" class="icon-button"
mat-icon-button *ngIf="selection.hasValue()" [disabled]="disabled"> mat-icon-button *ngIf="selection.hasValue()" [disabled]="disabled">
<mat-icon svgIcon="mdi_account_cancel"></mat-icon> <mat-icon>block</mat-icon>
</button> </button>
<button (click)="reactivateSelectedIdps()" matTooltip="{{'IDP.ACTIVATE' | translate}}" class="icon-button" <button (click)="reactivateSelectedIdps()" matTooltip="{{'IDP.ACTIVATE' | translate}}" class="icon-button"
mat-icon-button *ngIf="selection.hasValue()" [disabled]="disabled"> mat-icon-button *ngIf="selection.hasValue()" [disabled]="disabled">
<mat-icon svgIcon="mdi_account_check_outline"></mat-icon> <mat-icon>play_circle_outline</mat-icon>
</button> </button>
<button color="warn" (click)="removeSelectedIdps()" matTooltip="{{'IDP.DELETE' | translate}}" <button color="warn" (click)="removeSelectedIdps()" matTooltip="{{'IDP.DELETE' | translate}}"
class="icon-button" mat-icon-button *ngIf="selection.hasValue()" [disabled]="disabled"> class="icon-button" mat-icon-button *ngIf="selection.hasValue()" [disabled]="disabled">
<i class="las la-trash"></i> <i class="las la-trash"></i>
</button> </button>
<a class="add-button" [routerLink]="createRouterLink" color="primary" mat-raised-button [disabled]="disabled"> <a [routerLink]="createRouterLink" color="primary" mat-raised-button [disabled]="disabled">
<mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }} <mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }}
</a> </a>
</ng-template> </ng-template>
@@ -24,14 +24,14 @@
<th mat-header-cell *matHeaderCellDef> <th mat-header-cell *matHeaderCellDef>
<mat-checkbox color="primary" (change)="$event ? masterToggle() : null" <mat-checkbox color="primary" (change)="$event ? masterToggle() : null"
[checked]="selection.hasValue() && isAllSelected()" [checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()"> [indeterminate]="selection.hasValue() && !isAllSelected()"
[disabled]="serviceType==PolicyComponentServiceType.MGMT">
</mat-checkbox> </mat-checkbox>
</th> </th>
<td mat-cell *matCellDef="let idp"> <td mat-cell *matCellDef="let idp">
<mat-checkbox color="primary" (click)="$event.stopPropagation()" <mat-checkbox color="primary" (click)="$event.stopPropagation()"
[disabled]="serviceType==PolicyComponentServiceType.MGMT && idp?.providerType == IdpProviderType.IDPPROVIDERTYPE_SYSTEM"
(change)="$event ? selection.toggle(idp) : null" [checked]="selection.isSelected(idp)"> (change)="$event ? selection.toggle(idp) : null" [checked]="selection.isSelected(idp)">
<img *ngIf="idp?.logoSrc?.startsWith('https://'); else genAvatar" [src]="idp.logoSrc"
alt="ipp logo {{idp?.name}}" />
<ng-template #genAvatar> <ng-template #genAvatar>
<div class="avatar"> <div class="avatar">
<span>{{idp.name.charAt(0)}}</span> <span>{{idp.name.charAt(0)}}</span>
@@ -43,12 +43,12 @@
<ng-container matColumnDef="name"> <ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{ 'IDP.NAME' | translate }} </th> <th mat-header-cell *matHeaderCellDef> {{ 'IDP.NAME' | translate }} </th>
<td mat-cell *matCellDef="let idp"> {{idp?.name}} </td> <td [routerLink]="routerLinkForRow(idp)" mat-cell *matCellDef="let idp"> {{idp?.name}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="config"> <ng-container matColumnDef="config">
<th mat-header-cell *matHeaderCellDef> {{ 'IDP.CONFIG' | translate }} </th> <th mat-header-cell *matHeaderCellDef> {{ 'IDP.CONFIG' | translate }} </th>
<td mat-cell *matCellDef="let idp"> <td [routerLink]="routerLinkForRow(idp)" mat-cell *matCellDef="let idp">
<div *ngFor="let elem of idp?.oidcConfig | keyvalue" class="flex-row"> <div *ngFor="let elem of idp?.oidcConfig | keyvalue" class="flex-row">
<span class="key">{{elem.key}}:</span> <span class="key">{{elem.key}}:</span>
<span class="value">{{elem.value}}</span> <span class="value">{{elem.value}}</span>
@@ -58,28 +58,47 @@
<ng-container matColumnDef="state"> <ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef> {{ 'IDP.STATE' | translate }} </th> <th mat-header-cell *matHeaderCellDef> {{ 'IDP.STATE' | translate }} </th>
<td mat-cell *matCellDef="let idp"> {{ 'IDP.STATES.'+idp.state | translate }} </td> <td [routerLink]="routerLinkForRow(idp)" mat-cell *matCellDef="let idp">
{{ 'IDP.STATES.'+idp.state | translate }} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="creationDate"> <ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef> {{ 'IDP.CREATIONDATE' | translate }} </th> <th mat-header-cell *matHeaderCellDef> {{ 'IDP.CREATIONDATE' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let idp"> <td [routerLink]="routerLinkForRow(idp)" class="pointer" mat-cell *matCellDef="let idp">
{{idp.creationDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }} </td> {{idp.creationDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="changeDate"> <ng-container matColumnDef="changeDate">
<th mat-header-cell *matHeaderCellDef> {{ 'IDP.CHANGEDATE' | translate }} </th> <th mat-header-cell *matHeaderCellDef> {{ 'IDP.CHANGEDATE' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let idp"> <td [routerLink]="routerLinkForRow(idp)" class="pointer" mat-cell *matCellDef="let idp">
{{idp.changeDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }} </td> {{idp.changeDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }} </td>
</ng-container> </ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <ng-container matColumnDef="type">
<tr class="data-row" mat-row *matRowDef="let row; columns: displayedColumns;" <th mat-header-cell *matHeaderCellDef> {{ 'IDP.TYPE' | translate }} </th>
[routerLink]="row.id ? [serviceType == PolicyComponentServiceType.ADMIN ? '/iam' : '/org', 'idp', row.id ]: null"> <td [routerLink]="routerLinkForRow(idp)" class="pointer" mat-cell *matCellDef="let idp">
</tr> {{'IDP.TYPES.'+idp.providerType | translate }} </td>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let idp">
<button
[disabled]="serviceType==PolicyComponentServiceType.MGMT && idp?.providerType == IdpProviderType.IDPPROVIDERTYPE_SYSTEM"
mat-icon-button color="warn" matTooltip="{{'IAM.VIEWS.CLEAR' | translate}}"
(click)="removeIdp(idp)">
<i class="las la-trash"></i>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight"
[ngClass]="{'disabled': serviceType==PolicyComponentServiceType.MGMT && row?.providerType == IdpProviderType.IDPPROVIDERTYPE_SYSTEM}"
mat-row *matRowDef="let row; columns: displayedColumns;">
</tr>
</table> </table>
<mat-paginator #paginator class="paginator" [length]="idpResult?.totalResult || 0" [pageSize]="10"
[pageSizeOptions]="[5, 10, 20]" (page)="changePage($event)"></mat-paginator>
</div> </div>
<mat-paginator #paginator class="paginator" [length]="idpResult?.totalResult || 0" [pageSize]="10"
[pageSizeOptions]="[5, 10, 20]" (page)="changePage($event)"></mat-paginator>
</app-refresh-table> </app-refresh-table>

View File

@@ -1,11 +1,10 @@
.table-wrapper { .table-wrapper {
overflow: auto; overflow: auto;
width: 100%;
.table, .table,
.paginator { .paginator {
width: 100%;
td, td,
th { th {
padding: 0 1rem; padding: 0 1rem;
@@ -19,23 +18,29 @@
padding-right: 0; padding-right: 0;
} }
} }
.data-row {
cursor: pointer;
&:hover {
background-color: #ffffff05;
}
}
} }
} }
td {
outline: none;
}
tr { tr {
outline: none; outline: none;
}
.add-button { &.disabled * {
border-radius: .5rem; opacity: .8;
}
button {
visibility: hidden;
}
&:hover {
button {
visibility: visible;
}
}
} }
.avatar { .avatar {
@@ -57,14 +62,22 @@ tr {
.flex-row { .flex-row {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
padding: 2px 0;
width: 250px;
.key { .key {
margin-right: 1rem; margin-right: .5rem;
font-size: 14px; font-size: 12px;
flex: 1 0 50%;
overflow: hidden;
text-overflow: ellipsis;
} }
.value { .value {
font-size: 14px; font-size: 12px;
flex: 1 0 50%;
overflow: hidden;
text-overflow: ellipsis;
} }
&:first-child { &:first-child {

View File

@@ -1,17 +1,19 @@
import { SelectionModel } from '@angular/cdk/collections'; import { SelectionModel } from '@angular/cdk/collections';
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatPaginator, PageEvent } from '@angular/material/paginator'; import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
import { IdpSearchResponse as AdminIdpSearchResponse, IdpView as AdminIdpView } from 'src/app/proto/generated/admin_pb'; import { IdpSearchResponse as AdminIdpSearchResponse, IdpView as AdminIdpView } from 'src/app/proto/generated/admin_pb';
import { IdpView as MgmtIdpView } from 'src/app/proto/generated/management_pb'; import { IdpProviderType, IdpView as MgmtIdpView } from 'src/app/proto/generated/management_pb';
import { AdminService } from 'src/app/services/admin.service'; import { AdminService } from 'src/app/services/admin.service';
import { ManagementService } from 'src/app/services/mgmt.service'; import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
import { PolicyComponentServiceType } from '../policies/policy-component-types.enum'; import { PolicyComponentServiceType } from '../policies/policy-component-types.enum';
import { WarnDialogComponent } from '../warn-dialog/warn-dialog.component';
@Component({ @Component({
selector: 'app-idp-table', selector: 'app-idp-table',
@@ -31,12 +33,13 @@ export class IdpTableComponent implements OnInit {
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false); private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable(); public loading$: Observable<boolean> = this.loadingSubject.asObservable();
public PolicyComponentServiceType: any = PolicyComponentServiceType; public PolicyComponentServiceType: any = PolicyComponentServiceType;
public IdpProviderType: any = IdpProviderType;
@Input() public displayedColumns: string[] = ['select', 'name', 'config', 'creationDate', 'changeDate', 'state']; @Input() public displayedColumns: string[] = ['select', 'name', 'config', 'creationDate', 'changeDate', 'state'];
@Output() public changedSelection: EventEmitter<Array<AdminIdpView.AsObject | MgmtIdpView.AsObject>> @Output() public changedSelection: EventEmitter<Array<AdminIdpView.AsObject | MgmtIdpView.AsObject>>
= new EventEmitter(); = new EventEmitter();
constructor(public translate: TranslateService, private toast: ToastService) { constructor(public translate: TranslateService, private toast: ToastService, private dialog: MatDialog) {
this.selection.changed.subscribe(() => { this.selection.changed.subscribe(() => {
this.changedSelection.emit(this.selection.selected); this.changedSelection.emit(this.selection.selected);
}); });
@@ -44,6 +47,13 @@ export class IdpTableComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.getData(10, 0); this.getData(10, 0);
if (this.serviceType === PolicyComponentServiceType.MGMT) {
this.displayedColumns = ['select', 'name', 'config', 'creationDate', 'changeDate', 'state', 'type'];
}
if (!this.disabled) {
this.displayedColumns.push('actions');
}
} }
public isAllSelected(): boolean { public isAllSelected(): boolean {
@@ -64,47 +74,79 @@ export class IdpTableComponent implements OnInit {
} }
public deactivateSelectedIdps(): void { public deactivateSelectedIdps(): void {
this.selection.clear();
Promise.all(this.selection.selected.map(value => { Promise.all(this.selection.selected.map(value => {
return this.service.DeactivateIdpConfig(value.id); return this.service.DeactivateIdpConfig(value.id);
})).then(() => { })).then(() => {
this.toast.showInfo('USER.TOAST.SELECTEDDEACTIVATED', true); this.toast.showInfo('IDP.TOAST.SELECTEDDEACTIVATED', true);
this.getData(10, 0); this.refreshPage();
}); });
} }
public reactivateSelectedIdps(): void { public reactivateSelectedIdps(): void {
this.selection.clear();
Promise.all(this.selection.selected.map(value => { Promise.all(this.selection.selected.map(value => {
return this.service.ReactivateIdpConfig(value.id); return this.service.ReactivateIdpConfig(value.id);
})).then(() => { })).then(() => {
this.toast.showInfo('USER.TOAST.SELECTEDREACTIVATED', true); this.toast.showInfo('IDP.TOAST.SELECTEDREACTIVATED', true);
this.getData(10, 0); this.refreshPage();
}); });
} }
public removeSelectedIdps(): void { public removeSelectedIdps(): void {
Promise.all(this.selection.selected.map(value => { const dialogRef = this.dialog.open(WarnDialogComponent, {
return this.service.RemoveIdpConfig(value.id); data: {
})).then(() => { confirmKey: 'ACTIONS.DELETE',
this.toast.showInfo('USER.TOAST.SELECTEDDEACTIVATED', true); cancelKey: 'ACTIONS.CANCEL',
this.getData(10, 0); titleKey: 'IDP.DELETE_SELECTION_TITLE',
descriptionKey: 'IDP.DELETE_SELECTION_DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (resp) {
this.selection.clear();
Promise.all(this.selection.selected.map(value => {
return this.service.RemoveIdpConfig(value.id);
})).then(() => {
this.toast.showInfo('IDP.TOAST.SELECTEDDEACTIVATED', true);
this.refreshPage();
});
}
});
}
public removeIdp(idp: AdminIdpView.AsObject | MgmtIdpView.AsObject): void {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'IDP.DELETE_TITLE',
descriptionKey: 'IDP.DELETE_DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (resp) {
this.service.RemoveIdpConfig(idp.id).then(() => {
this.toast.showInfo('IDP.TOAST.REMOVED', true);
setTimeout(() => {
this.refreshPage();
}, 1000);
});
}
}); });
} }
private async getData(limit: number, offset: number): Promise<void> { private async getData(limit: number, offset: number): Promise<void> {
this.loadingSubject.next(true); this.loadingSubject.next(true);
// let query: AdminIdpSearchQuery | MgmtIdpSearchQuery;
// if (this.service instanceof AdminService) {
// query = new AdminIdpSearchQuery();
// query.setKey(AdminIdpSearchKey.)
// } else if (this.service instanceof ManagementService) {
// return ['/org', 'idp', 'create'];
// }
this.service.SearchIdps(limit, offset).then(resp => { this.service.SearchIdps(limit, offset).then(resp => {
this.idpResult = resp.toObject(); this.idpResult = resp.toObject();
this.dataSource.data = this.idpResult.resultList; this.dataSource.data = this.idpResult.resultList;
console.log(this.idpResult.resultList);
this.loadingSubject.next(false); this.loadingSubject.next(false);
}).catch(error => { }).catch(error => {
this.toast.showError(error); this.toast.showError(error);
@@ -123,4 +165,21 @@ export class IdpTableComponent implements OnInit {
return ['/org', 'idp', 'create']; return ['/org', 'idp', 'create'];
} }
} }
public routerLinkForRow(row: MgmtIdpView.AsObject | AdminIdpView.AsObject): any {
if (row.id) {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
switch ((row as MgmtIdpView.AsObject).providerType) {
case IdpProviderType.IDPPROVIDERTYPE_SYSTEM:
return ['/iam', 'idp', row.id];
case IdpProviderType.IDPPROVIDERTYPE_ORG:
return ['/org', 'idp', row.id];
}
break;
case PolicyComponentServiceType.ADMIN:
return ['/iam', 'idp', row.id];
}
}
}
} }

View File

@@ -11,8 +11,9 @@ import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module'; import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { RefreshTableModule } from 'src/app/modules/refresh-table/refresh-table.module'; import { RefreshTableModule } from 'src/app/modules/refresh-table/refresh-table.module';
import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe.module'; import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module';
import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe.module'; import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module';
import { TruncatePipeModule } from 'src/app/pipes/truncate-pipe/truncate-pipe.module';
import { IdpTableComponent } from './idp-table.component'; import { IdpTableComponent } from './idp-table.component';
@@ -34,6 +35,7 @@ import { IdpTableComponent } from './idp-table.component';
RouterModule, RouterModule,
RefreshTableModule, RefreshTableModule,
HasRoleModule, HasRoleModule,
TruncatePipeModule,
], ],
exports: [ exports: [
IdpTableComponent, IdpTableComponent,

View File

@@ -1,91 +1,99 @@
<app-detail-layout [backRouterLink]="backroutes" [title]="'IDP.DETAIL.TITLE' | translate" <app-detail-layout [backRouterLink]="backroutes" [title]="'IDP.DETAIL.TITLE' | translate"
[description]="'IDP.DETAIL.DESCRIPTION' | translate"> [description]="'IDP.DETAIL.DESCRIPTION' | translate">
<div class="container"> <div class="container">
<form (ngSubmit)="updateIdp()"> <form (ngSubmit)="updateIdp()">
<ng-container [formGroup]="idpForm"> <ng-container [formGroup]="idpForm">
<div class="content"> <div class="content">
<mat-form-field appearance="outline" class="formfield"> <mat-form-field appearance="outline" class="formfield">
<mat-label>{{ 'IDP.ID' | translate }}</mat-label> <mat-label>{{ 'IDP.ID' | translate }}</mat-label>
<input matInput formControlName="id" /> <input matInput formControlName="id" />
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline" class="formfield"> <mat-form-field appearance="outline" class="formfield">
<mat-label>{{ 'IDP.NAME' | translate }}</mat-label> <mat-label>{{ 'IDP.NAME' | translate }}</mat-label>
<input matInput formControlName="name" /> <input matInput formControlName="name" />
</mat-form-field> </mat-form-field>
<!--<mat-form-field appearance="outline" class="formfield"> <mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'IDP.LOGOSRC' | translate }}</mat-label> <mat-label>{{ 'IDP.STYLE' | translate }}</mat-label>
<input matInput formControlName="logoSrc" /> <mat-select formControlName="stylingType">
</mat-form-field>--> <mat-option *ngFor="let field of styleFields" [value]="field">
{{ 'IDP.STYLEFIELD.'+field | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</ng-container>
<div class="btn-wrapper">
<button color="primary" mat-raised-button class="continue-button" [disabled]="idpForm.invalid"
type="submit">
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div> </div>
</form>
<ng-container *ngIf="oidcConfigForm">
<h2>{{'IDP.DETAIL.OIDC.TITLE' | translate}}</h2>
<p>{{'IDP.DETAIL.OIDC.DESCRIPTION' | translate}}</p>
<form (ngSubmit)="updateOidcConfig()">
<ng-container [formGroup]="oidcConfigForm">
<div class="content">
<mat-form-field appearance="outline" class="formfield">
<mat-label>{{ 'IDP.ISSUER' | translate }}</mat-label>
<input matInput formControlName="issuer" />
</mat-form-field>
<mat-form-field appearance="outline" class="formfield">
<mat-label>{{ 'IDP.CLIENTID' | translate }}</mat-label>
<input matInput formControlName="clientId" />
</mat-form-field>
<mat-checkbox class="desc" [(ngModel)]="showIdSecretSection"
[ngModelOptions]="{standalone: true}">
Update Client Secret
</mat-checkbox>
<mat-form-field appearance="outline" class="formfield" *ngIf="showIdSecretSection">
<mat-label>{{ 'IDP.CLIENTSECRET' | translate }}</mat-label>
<input matInput formControlName="clientSecret" />
</mat-form-field>
<mat-form-field appearance="outline" class="formfield fullwidth">
<mat-label>{{ 'IDP.SCOPESLIST' | translate }}</mat-label>
<mat-chip-list #chipScopesList aria-label="scope selection">
<mat-chip class="chip" *ngFor="let scope of scopesList?.value" selectable="false"
removable (removed)="removeScope(scope)">
{{scope}} <mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input [matChipInputFor]="chipScopesList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" [matChipInputAddOnBlur]="true"
(matChipInputTokenEnd)="addScope($event)">
</mat-chip-list>
</mat-form-field>
<mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'IDP.IDPDISPLAYNAMMAPPING' | translate }}</mat-label>
<mat-select formControlName="idpDisplayNameMapping">
<mat-option *ngFor="let field of mappingFields" [value]="field">
{{ 'IDP.MAPPINGFIELD.'+field | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'IDP.USERNAMEMAPPING' | translate }}</mat-label>
<mat-select formControlName="usernameMapping">
<mat-option *ngFor="let field of mappingFields" [value]="field">
{{ 'IDP.MAPPINGFIELD.'+field | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</ng-container>
<div class="btn-wrapper">
<button color="primary" mat-raised-button class="continue-button"
[disabled]="oidcConfigForm.invalid" type="submit">
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div>
</form>
</ng-container> </ng-container>
</div>
<button color="primary" mat-raised-button class="continue-button" [disabled]="idpForm.invalid" type="submit"> </app-detail-layout>
{{ 'ACTIONS.SAVE' | translate }}
</button>
</form>
<h2 *ngIf="oidcConfigForm">{{'IDP.DETAIL.OIDC.TITLE' | translate}}</h2>
<form (ngSubmit)="updateOidcConfig()" *ngIf="oidcConfigForm">
<ng-container [formGroup]="oidcConfigForm">
<div class="content">
<mat-form-field appearance="outline" class="formfield">
<mat-label>{{ 'IDP.ISSUER' | translate }}</mat-label>
<input matInput formControlName="issuer" />
</mat-form-field>
</div>
<div class="content">
<mat-form-field appearance="outline" class="formfield">
<mat-label>{{ 'IDP.CLIENTID' | translate }}</mat-label>
<input matInput formControlName="clientId" />
</mat-form-field>
</div>
<div class="content">
<mat-checkbox class="desc" [(ngModel)]="showIdSecretSection" [ngModelOptions]="{standalone: true}">
Update Client Secret
</mat-checkbox>
<mat-form-field appearance="outline" class="formfield" *ngIf="showIdSecretSection">
<mat-label>{{ 'IDP.CLIENTSECRET' | translate }}</mat-label>
<input matInput formControlName="clientSecret" />
</mat-form-field>
</div>
<div class="content">
<mat-form-field appearance="outline" class="formfield">
<mat-label>{{ 'IDP.SCOPESLIST' | translate }}</mat-label>
<mat-chip-list #chipScopesList aria-label="scope selection" >
<mat-chip class="chip" *ngFor="let scope of scopesList?.value" selectable="false" removable
(removed)="removeScope(scope)">
{{scope}} <mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input [matChipInputFor]="chipScopesList" [matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[matChipInputAddOnBlur]="true" (matChipInputTokenEnd)="addScope($event)">
</mat-chip-list>
</mat-form-field>
</div>
<div class="content">
<mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'IDP.IDPDISPLAYNAMMAPPING' | translate }}</mat-label>
<mat-select formControlName="idpDisplayNameMapping">
<mat-option *ngFor="let field of mappingFields" [value]="field">
{{ 'IDP.MAPPINTFIELD.'+field | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'IDP.USERNAMEMAPPING' | translate }}</mat-label>
<mat-select formControlName="usernameMapping">
<mat-option *ngFor="let field of mappingFields" [value]="field">
{{ 'IDP.MAPPINTFIELD.'+field | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</ng-container>
<button color="primary" mat-raised-button class="continue-button" [disabled]="oidcConfigForm.invalid" type="submit">
{{ 'ACTIONS.SAVE' | translate }}
</button>
</form>
</div>
</app-detail-layout>

View File

@@ -9,6 +9,7 @@
.content { .content {
display: flex; display: flex;
flex-direction: row;
margin: 0 -.5rem; margin: 0 -.5rem;
flex-wrap: wrap; flex-wrap: wrap;
@@ -16,27 +17,35 @@
flex-basis: 100%; flex-basis: 100%;
margin: 0 .5rem; margin: 0 .5rem;
margin-bottom: 1rem; margin-bottom: 1rem;
color: #8795a1; color: var(--grey);
} }
.formfield { .formfield {
flex: 1 0 auto; flex: 1 1 auto;
margin: 0 .5rem; margin: 0 .5rem;
&.fullwidth {
flex-basis: 100%;
}
@media only screen and (max-width: 450px) { @media only screen and (max-width: 450px) {
flex-basis: 100%; flex-basis: 100%;
} }
} }
} }
.continue-button { .btn-wrapper {
margin-bottom: 4rem; display: flex;
display: block; justify-content: flex-end;
padding: .5rem 4rem;
border-radius: .5rem;
@media only screen and (max-width: 450px) { .continue-button {
margin-top: 1rem; margin-bottom: 4rem;
margin-bottom: 2rem; display: block;
padding: .5rem 4rem;
@media only screen and (max-width: 450px) {
margin-top: 1rem;
margin-bottom: 2rem;
}
} }
} }

View File

@@ -1,20 +1,22 @@
import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes'; import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes';
import { Location } from '@angular/common'; import { Location } from '@angular/common';
import {Component, Injector, Input, OnDestroy, OnInit, Type} from '@angular/core'; import { Component, Injector, OnDestroy, OnInit, Type } from '@angular/core';
import { AbstractControl, FormControl, FormGroup, Validators } from '@angular/forms'; import { AbstractControl, FormControl, FormGroup, Validators } from '@angular/forms';
import { MatChipInputEvent } from '@angular/material/chips'; import { MatChipInputEvent } from '@angular/material/chips';
import { ActivatedRoute, Params } from '@angular/router'; import { ActivatedRoute, Params } from '@angular/router';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { switchMap, take } from 'rxjs/operators'; import { switchMap, take } from 'rxjs/operators';
import { import {
OIDCMappingField as authMappingFields, IdpStylingType as adminIdpStylingType,
OidcIdpConfigUpdate as AdminOidcIdpConfigUpdate, IdpUpdate as AdminIdpConfigUpdate,
IdpUpdate as AdminIdpConfigUpdate, OidcIdpConfigUpdate as AdminOidcIdpConfigUpdate,
OIDCMappingField as adminMappingFields,
} from 'src/app/proto/generated/admin_pb'; } from 'src/app/proto/generated/admin_pb';
import { import {
OIDCMappingField as mgmtMappingFields, IdpStylingType as mgmtIdpStylingType,
OidcIdpConfigUpdate as MgmtOidcIdpConfigUpdate, IdpUpdate as MgmtIdpConfigUpdate,
IdpUpdate as MgmtIdpConfigUpdate, OidcIdpConfigUpdate as MgmtOidcIdpConfigUpdate,
OIDCMappingField as mgmtMappingFields,
} from 'src/app/proto/generated/management_pb'; } from 'src/app/proto/generated/management_pb';
import { AdminService } from 'src/app/services/admin.service'; import { AdminService } from 'src/app/services/admin.service';
import { ManagementService } from 'src/app/services/mgmt.service'; import { ManagementService } from 'src/app/services/mgmt.service';
@@ -28,7 +30,9 @@ import { PolicyComponentServiceType } from '../policies/policy-component-types.e
styleUrls: ['./idp.component.scss'], styleUrls: ['./idp.component.scss'],
}) })
export class IdpComponent implements OnInit, OnDestroy { export class IdpComponent implements OnInit, OnDestroy {
public mappingFields: mgmtMappingFields[] | authMappingFields[] = []; public mappingFields: mgmtMappingFields[] | adminMappingFields[] = [];
public styleFields: mgmtIdpStylingType[] | adminIdpStylingType[] = [];
public showIdSecretSection: boolean = false; public showIdSecretSection: boolean = false;
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT; public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
private service!: ManagementService | AdminService; private service!: ManagementService | AdminService;
@@ -41,7 +45,6 @@ export class IdpComponent implements OnInit, OnDestroy {
public oidcConfigForm!: FormGroup; public oidcConfigForm!: FormGroup;
constructor( constructor(
// private router: Router,
private toast: ToastService, private toast: ToastService,
private injector: Injector, private injector: Injector,
private route: ActivatedRoute, private route: ActivatedRoute,
@@ -50,33 +53,38 @@ export class IdpComponent implements OnInit, OnDestroy {
this.idpForm = new FormGroup({ this.idpForm = new FormGroup({
id: new FormControl({ disabled: true, value: '' }, [Validators.required]), id: new FormControl({ disabled: true, value: '' }, [Validators.required]),
name: new FormControl('', [Validators.required]), name: new FormControl('', [Validators.required]),
logoSrc: new FormControl({ disabled: true, value: '' }, [Validators.required]), stylingType: new FormControl('', [Validators.required]),
}); });
this.oidcConfigForm = new FormGroup({ this.oidcConfigForm = new FormGroup({
clientId: new FormControl('', [Validators.required]), clientId: new FormControl('', [Validators.required]),
clientSecret: new FormControl(''), clientSecret: new FormControl(''),
issuer: new FormControl('', [Validators.required]), issuer: new FormControl('', [Validators.required]),
scopesList: new FormControl([], []), scopesList: new FormControl([], []),
idpDisplayNameMapping: new FormControl(0), idpDisplayNameMapping: new FormControl(0),
usernameMapping: new FormControl(0), usernameMapping: new FormControl(0),
}); });
this.route.data.pipe(switchMap(data => { this.route.data.pipe(switchMap(data => {
console.log(data.serviceType);
this.serviceType = data.serviceType; this.serviceType = data.serviceType;
switch (this.serviceType) { switch (this.serviceType) {
case PolicyComponentServiceType.MGMT: case PolicyComponentServiceType.MGMT:
this.service = this.injector.get(ManagementService as Type<ManagementService>); this.service = this.injector.get(ManagementService as Type<ManagementService>);
this.mappingFields = [ this.mappingFields = [
mgmtMappingFields.OIDCMAPPINGFIELD_PREFERRED_USERNAME, mgmtMappingFields.OIDCMAPPINGFIELD_PREFERRED_USERNAME,
mgmtMappingFields.OIDCMAPPINGFIELD_EMAIL]; mgmtMappingFields.OIDCMAPPINGFIELD_EMAIL];
this.styleFields = [
mgmtIdpStylingType.IDPSTYLINGTYPE_UNSPECIFIED,
mgmtIdpStylingType.IDPSTYLINGTYPE_GOOGLE];
break; break;
case PolicyComponentServiceType.ADMIN: case PolicyComponentServiceType.ADMIN:
this.service = this.injector.get(AdminService as Type<AdminService>); this.service = this.injector.get(AdminService as Type<AdminService>);
this.mappingFields = [ this.mappingFields = [
authMappingFields.OIDCMAPPINGFIELD_PREFERRED_USERNAME, adminMappingFields.OIDCMAPPINGFIELD_PREFERRED_USERNAME,
authMappingFields.OIDCMAPPINGFIELD_EMAIL]; adminMappingFields.OIDCMAPPINGFIELD_EMAIL];
this.styleFields = [
adminIdpStylingType.IDPSTYLINGTYPE_UNSPECIFIED,
adminIdpStylingType.IDPSTYLINGTYPE_GOOGLE];
break; break;
} }
@@ -85,11 +93,11 @@ export class IdpComponent implements OnInit, OnDestroy {
const { id } = params; const { id } = params;
if (id) { if (id) {
this.service.IdpByID(id).then(idp => { this.service.IdpByID(id).then(idp => {
const idpObject = idp.toObject(); const idpObject = idp.toObject();
this.idpForm.patchValue(idpObject); this.idpForm.patchValue(idpObject);
if (idpObject.oidcConfig) { if (idpObject.oidcConfig) {
this.oidcConfigForm.patchValue(idpObject.oidcConfig); this.oidcConfigForm.patchValue(idpObject.oidcConfig);
} }
}); });
} }
}); });
@@ -121,10 +129,10 @@ export class IdpComponent implements OnInit, OnDestroy {
req.setId(this.id?.value); req.setId(this.id?.value);
req.setName(this.name?.value); req.setName(this.name?.value);
req.setLogoSrc(this.logoSrc?.value); req.setStylingType(this.stylingType?.value);
this.service.UpdateIdp(req).then((idp) => { this.service.UpdateIdp(req).then((idp) => {
this.toast.showInfo('IDP.TOAST.SAVED', true); this.toast.showInfo('IDP.TOAST.SAVED', true);
// this.router.navigate(['idp', ]); // this.router.navigate(['idp', ]);
}).catch(error => { }).catch(error => {
this.toast.showError(error); this.toast.showError(error);
@@ -132,31 +140,31 @@ export class IdpComponent implements OnInit, OnDestroy {
} }
public updateOidcConfig(): void { public updateOidcConfig(): void {
let req: AdminOidcIdpConfigUpdate | MgmtOidcIdpConfigUpdate; let req: AdminOidcIdpConfigUpdate | MgmtOidcIdpConfigUpdate;
switch (this.serviceType) { switch (this.serviceType) {
case PolicyComponentServiceType.MGMT: case PolicyComponentServiceType.MGMT:
req = new MgmtOidcIdpConfigUpdate(); req = new MgmtOidcIdpConfigUpdate();
break; break;
case PolicyComponentServiceType.ADMIN: case PolicyComponentServiceType.ADMIN:
req = new AdminOidcIdpConfigUpdate(); req = new AdminOidcIdpConfigUpdate();
break; break;
} }
req.setIdpId(this.id?.value); req.setIdpId(this.id?.value);
req.setClientId(this.clientId?.value); req.setClientId(this.clientId?.value);
req.setClientSecret(this.clientSecret?.value); req.setClientSecret(this.clientSecret?.value);
req.setIssuer(this.issuer?.value); req.setIssuer(this.issuer?.value);
req.setScopesList(this.scopesList?.value); req.setScopesList(this.scopesList?.value);
req.setUsernameMapping(this.usernameMapping?.value); req.setUsernameMapping(this.usernameMapping?.value);
req.setIdpDisplayNameMapping(this.idpDisplayNameMapping?.value); req.setIdpDisplayNameMapping(this.idpDisplayNameMapping?.value);
this.service.UpdateOidcIdpConfig(req).then((oidcConfig) => { this.service.UpdateOidcIdpConfig(req).then((oidcConfig) => {
this.toast.showInfo('IDP.TOAST.SAVED', true); this.toast.showInfo('IDP.TOAST.SAVED', true);
// this.router.navigate(['idp', ]); // this.router.navigate(['idp', ]);
}).catch(error => { }).catch(error => {
this.toast.showError(error); this.toast.showError(error);
}); });
} }
public close(): void { public close(): void {
@@ -190,12 +198,10 @@ export class IdpComponent implements OnInit, OnDestroy {
public get backroutes(): string[] { public get backroutes(): string[] {
switch (this.serviceType) { switch (this.serviceType) {
case PolicyComponentServiceType.MGMT: case PolicyComponentServiceType.MGMT:
return ['/org', 'policy', 'login']; return ['/org', 'policy', 'login'];
case PolicyComponentServiceType.ADMIN: case PolicyComponentServiceType.ADMIN:
return ['/iam', 'policy', 'login']; return ['/iam', 'policy', 'login'];
break;
} }
return [];
} }
public get id(): AbstractControl | null { public get id(): AbstractControl | null {
@@ -206,8 +212,8 @@ export class IdpComponent implements OnInit, OnDestroy {
return this.idpForm.get('name'); return this.idpForm.get('name');
} }
public get logoSrc(): AbstractControl | null { public get stylingType(): AbstractControl | null {
return this.idpForm.get('logoSrc'); return this.idpForm.get('stylingType');
} }
public get clientId(): AbstractControl | null { public get clientId(): AbstractControl | null {
@@ -227,10 +233,10 @@ export class IdpComponent implements OnInit, OnDestroy {
} }
public get idpDisplayNameMapping(): AbstractControl | null { public get idpDisplayNameMapping(): AbstractControl | null {
return this.oidcConfigForm.get('idpDisplayNameMapping'); return this.oidcConfigForm.get('idpDisplayNameMapping');
} }
public get usernameMapping(): AbstractControl | null { public get usernameMapping(): AbstractControl | null {
return this.oidcConfigForm.get('usernameMapping'); return this.oidcConfigForm.get('usernameMapping');
} }
} }

View File

@@ -0,0 +1,98 @@
<app-refresh-table *ngIf="dataSource" (refreshed)="changePage()" [dataSize]="dataSource.totalResult"
[timestamp]="dataSource.viewTimestamp" [selection]="selection" [loading]="dataSource?.loading$ | async">
<ng-container actions *ngIf="selection.hasValue()">
<ng-content select="[selectactions]"></ng-content>
</ng-container>
<div actions>
<ng-content select="[writeactions]"></ng-content>
</div>
<div class="table-wrapper">
<table mat-table class="table" aria-label="Elements" [dataSource]="dataSource">
<ng-container matColumnDef="select">
<th class="selection" mat-header-cell *matHeaderCellDef>
<mat-checkbox [disabled]="!canWrite" color="primary" (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 [disabled]="!canWrite" color="primary" (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)">
<app-avatar *ngIf="row?.displayName && row.firstName && row.lastName; else cog" class="avatar"
[name]="row.displayName" [size]="32">
</app-avatar>
<ng-template #cog>
<div class="sa-icon">
<i class="las la-user-cog"></i>
</div>
</ng-template>
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="userId">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.USERID' | translate }} </th>
<td class="pointer" [routerLink]="['/users', member.userId]" mat-cell *matCellDef="let member">
{{member.userId}} </td>
</ng-container>
<ng-container matColumnDef="firstname">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.FIRSTNAME' | translate }} </th>
<td class="pointer" [routerLink]="['/users', member.userId]" mat-cell *matCellDef="let member">
{{member.firstName}} </td>
</ng-container>
<ng-container matColumnDef="lastname">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.LASTNAME' | translate }} </th>
<td class="pointer" [routerLink]="['/users', member.userId]" mat-cell *matCellDef="let member">
{{member.lastName}} </td>
</ng-container>
<ng-container matColumnDef="username">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.USERNAME' | translate }} </th>
<td class="pointer" [routerLink]="['/users', member.userId]" mat-cell *matCellDef="let member">
{{member.userName}} </td>
</ng-container>
<ng-container matColumnDef="email">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.EMAIL' | translate }} </th>
<td class="pointer" [routerLink]="['/users', member.userId]" mat-cell *matCellDef="let member">
{{member.email}}
</td>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let view">
<button matTooltip="{{'ACTIONS.REMOVE' | translate}}" color="warn"
(click)="triggerDeleteMember(view)" mat-icon-button><i class="las la-trash"></i></button>
</td>
</ng-container>
<ng-container matColumnDef="roles">
<th mat-header-cell *matHeaderCellDef> {{ 'ROLESLABEL' | translate }} </th>
<td mat-cell *matCellDef="let member">
<mat-form-field class="form-field" appearance="outline">
<mat-label>{{ 'ROLESLABEL' | translate }}</mat-label>
<mat-select [(ngModel)]="member.rolesList" multiple [disabled]="!canWrite"
(selectionChange)="updateRoles.emit({member: member, change: $event})">
<mat-option *ngFor="let role of memberRoleOptions" [value]="role">
{{ role }}
</mat-option>
</mat-select>
</mat-form-field>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns;">
</tr>
</table>
</div>
<mat-paginator *ngIf="dataSource" class="paginator" #paginator [pageSize]="INITIALPAGESIZE"
[length]="dataSource.totalResult" [pageSizeOptions]="[25, 50, 100, 250]" (page)="changePage($event)">
</mat-paginator>
</app-refresh-table>

View File

@@ -1,9 +1,14 @@
.icon-button {
margin-right: .5rem;
}
.table-wrapper { .table-wrapper {
overflow: auto; overflow-x: auto;
.table, .table,
.paginator { .paginator {
width: 100%;
td, td,
th { th {
padding: .5rem; padding: .5rem;
@@ -22,20 +27,31 @@
width: 40px; width: 40px;
} }
.data-row {
&:hover {
background-color: #ffffff05;
}
}
.selection { .selection {
width: 50px; width: 50px;
max-width: 50px; max-width: 50px;
} }
}
.role { tr {
display: inline-block; button {
margin: .25rem; visibility: hidden;
}
&:hover {
button {
visibility: visible;
}
}
}
.sa-icon {
display: block;
width: 32px;
margin: 0 .5rem;
i {
margin: auto;
} }
} }
} }

View File

@@ -4,15 +4,15 @@ import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ProjectGrantMembersComponent } from './project-grant-members.component'; import { MembersTableComponent } from './members-table.component';
describe('ProjectMembersComponent', () => { describe('MembersTableComponent', () => {
let component: ProjectGrantMembersComponent; let component: MembersTableComponent;
let fixture: ComponentFixture<ProjectGrantMembersComponent>; let fixture: ComponentFixture<MembersTableComponent>;
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ProjectGrantMembersComponent], declarations: [MembersTableComponent],
imports: [ imports: [
NoopAnimationsModule, NoopAnimationsModule,
MatPaginatorModule, MatPaginatorModule,
@@ -23,7 +23,7 @@ describe('ProjectMembersComponent', () => {
})); }));
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(ProjectGrantMembersComponent); fixture = TestBed.createComponent(MembersTableComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@@ -0,0 +1,83 @@
import { SelectionModel } from '@angular/cdk/collections';
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSelectChange } from '@angular/material/select';
import { MatTable } from '@angular/material/table';
import { Observable, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { IamMembersDataSource } from 'src/app/pages/iam/iam-members/iam-members-datasource';
import { OrgMembersDataSource } from 'src/app/pages/orgs/org-members/org-members-datasource';
import { IamMemberView } from 'src/app/proto/generated/admin_pb';
import { OrgMemberView, ProjectMemberView } from 'src/app/proto/generated/management_pb';
import { ProjectMembersDataSource } from '../project-members/project-members-datasource';
type View = OrgMemberView.AsObject | ProjectMemberView.AsObject | IamMemberView.AsObject;
type MemberDatasource = OrgMembersDataSource | ProjectMembersDataSource | IamMembersDataSource;
@Component({
selector: 'app-members-table',
templateUrl: './members-table.component.html',
styleUrls: ['./members-table.component.scss'],
})
export class MembersTableComponent implements OnInit, OnDestroy {
public INITIALPAGESIZE: number = 25;
@Input() public canDelete: boolean = false;
@Input() public canWrite: boolean = false;
@ViewChild(MatPaginator) public paginator!: MatPaginator;
@ViewChild(MatTable) public table!: MatTable<View>;
@Input() public dataSource!: MemberDatasource;
public selection: SelectionModel<any> = new SelectionModel<any>(true, []);
@Input() public memberRoleOptions: string[] = [];
@Input() public factoryLoadFunc!: Function;
@Input() public refreshTrigger!: Observable<void>;
@Output() public updateRoles: EventEmitter<{ member: View, change: MatSelectChange; }> = new EventEmitter();
@Output() public changedSelection: EventEmitter<any[]> = new EventEmitter();
@Output() public deleteMember: EventEmitter<View> = new EventEmitter();
private destroyed: Subject<void> = new Subject();
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
public displayedColumns: string[] = ['select', 'userId', 'firstname', 'lastname', 'username', 'email', 'roles'];
constructor() {
this.selection.changed.pipe(takeUntil(this.destroyed)).subscribe(_ => {
this.changedSelection.emit(this.selection.selected);
});
}
public ngOnInit(): void {
this.refreshTrigger.pipe(takeUntil(this.destroyed)).subscribe(() => {
this.changePage(this.paginator);
});
if (this.canDelete) {
this.displayedColumns.push('actions');
}
}
public ngOnDestroy(): void {
this.destroyed.next();
}
public isAllSelected(): boolean {
const numSelected = this.selection.selected.length;
const numRows = this.dataSource.membersSubject.value.length;
return numSelected === numRows;
}
public masterToggle(): void {
this.isAllSelected() ?
this.selection.clear() :
this.dataSource.membersSubject.value.forEach(row => this.selection.select(row));
}
public changePage(event?: PageEvent | MatPaginator): any {
this.selection.clear();
return this.factoryLoadFunc(event ?? this.paginator);
}
public triggerDeleteMember(member: any): void {
this.deleteMember.emit(member);
}
}

View File

@@ -0,0 +1,45 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSelectModule } from '@angular/material/select';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { MatTooltipModule } from '@angular/material/tooltip';
import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { AvatarModule } from '../avatar/avatar.module';
import { RefreshTableModule } from '../refresh-table/refresh-table.module';
import { MembersTableComponent } from './members-table.component';
@NgModule({
declarations: [
MembersTableComponent,
],
imports: [
CommonModule,
MatFormFieldModule,
MatSelectModule,
MatCheckboxModule,
MatIconModule,
MatTableModule,
MatPaginatorModule,
MatSortModule,
MatTooltipModule,
FormsModule,
TranslateModule,
RefreshTableModule,
RouterModule,
AvatarModule,
MatButtonModule,
],
exports: [
MembersTableComponent,
],
})
export class MembersTableModule { }

View File

@@ -2,7 +2,7 @@
display: flex; display: flex;
height: 100%; height: 100%;
overflow-x: hidden; overflow-x: hidden;
transition: all .2s ease-in-out; transition: all .3s cubic-bezier(.645, .045, .355, 1);
.main-content { .main-content {
display: relative; display: relative;

View File

@@ -1,6 +1,7 @@
.validation-col { .validation-col {
display: flex wrap; display: flex;
flex-wrap: wrap;
padding: 1rem 0; padding: 1rem 0;
width: 100%; width: 100%;
@@ -16,7 +17,7 @@
span { span {
font-size: 14px; font-size: 14px;
color: #8795a1; color: var(--grey);
} }
.sp-wrapper { .sp-wrapper {

View File

@@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { LabelPolicyComponent } from './label-policy.component';
const routes: Routes = [
{
path: '',
component: LabelPolicyComponent,
data: {
animation: 'DetailPage',
},
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class LabelPolicyRoutingModule { }

View File

@@ -0,0 +1,20 @@
<app-detail-layout [backRouterLink]="['/iam']" [title]="'POLICY.LABEL.TITLE' | translate"
[description]="'POLICY.LABEL.DESCRIPTION' | translate">
<div class="content" *ngIf="labelData">
<mat-form-field class="form-field" appearance="outline">
<mat-label>{{'POLICY.LABEL.PRIMARYCOLOR' | translate}}</mat-label>
<input [(ngModel)]="labelData.primaryColor" matInput />
</mat-form-field>
<mat-form-field class="form-field" appearance="outline">
<mat-label>{{'POLICY.LABEL.SECONDARYCOLOR' | translate}}</mat-label>
<input [(ngModel)]="labelData.secondaryColor" matInput />
</mat-form-field>
</div>
<div class="btn-container">
<button (click)="savePolicy()" color="primary" type="submit"
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
</div>
</app-detail-layout>

View File

@@ -0,0 +1,24 @@
.content {
padding-top: 1rem;
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: 0 -.5rem;
.form-field {
flex: 1;
margin: 0 .5rem;
}
}
.btn-container {
display: flex;
justify-content: flex-end;
width: 100%;
button {
margin-top: 3rem;
display: block;
padding: .5rem 4rem;
}
}

View File

@@ -1,20 +1,20 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { IamPolicyGridComponent } from './iam-policy-grid.component'; import { LabelPolicyComponent } from './label-policy.component';
describe('IamPolicyGridComponent', () => { describe('LabelPolicyComponent', () => {
let component: IamPolicyGridComponent; let component: LabelPolicyComponent;
let fixture: ComponentFixture<IamPolicyGridComponent>; let fixture: ComponentFixture<LabelPolicyComponent>;
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [IamPolicyGridComponent], declarations: [LabelPolicyComponent],
}) })
.compileComponents(); .compileComponents();
})); }));
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(IamPolicyGridComponent); fixture = TestBed.createComponent(LabelPolicyComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@@ -0,0 +1,54 @@
import { Component, OnDestroy } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import { DefaultLabelPolicyUpdate, DefaultLabelPolicyView } from 'src/app/proto/generated/admin_pb';
import { AdminService } from 'src/app/services/admin.service';
import { ToastService } from 'src/app/services/toast.service';
import { PolicyComponentServiceType } from '../policy-component-types.enum';
@Component({
selector: 'app-label-policy',
templateUrl: './label-policy.component.html',
styleUrls: ['./label-policy.component.scss'],
})
export class LabelPolicyComponent implements OnDestroy {
public labelData!: DefaultLabelPolicyView.AsObject;
private sub: Subscription = new Subscription();
public PolicyComponentServiceType: any = PolicyComponentServiceType;
constructor(
private route: ActivatedRoute,
private toast: ToastService,
private adminService: AdminService,
) {
this.route.params.subscribe(() => {
this.getData().then(data => {
if (data) {
this.labelData = data.toObject();
}
});
});
}
public ngOnDestroy(): void {
this.sub.unsubscribe();
}
private async getData(): Promise<DefaultLabelPolicyView> {
return this.adminService.GetDefaultLabelPolicy();
}
public savePolicy(): void {
const req = new DefaultLabelPolicyUpdate();
req.setPrimaryColor(this.labelData.primaryColor);
req.setSecondaryColor(this.labelData.secondaryColor);
this.adminService.UpdateDefaultLabelPolicy(req).then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
}).catch(error => {
this.toast.showError(error);
});
}
}

View File

@@ -11,13 +11,13 @@ import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module'; import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.module'; import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.module';
import { PasswordIamPolicyRoutingModule } from './password-iam-policy-routing.module'; import { LabelPolicyRoutingModule } from './label-policy-routing.module';
import { PasswordIamPolicyComponent } from './password-iam-policy.component'; import { LabelPolicyComponent } from './label-policy.component';
@NgModule({ @NgModule({
declarations: [PasswordIamPolicyComponent], declarations: [LabelPolicyComponent],
imports: [ imports: [
PasswordIamPolicyRoutingModule, LabelPolicyRoutingModule,
CommonModule, CommonModule,
FormsModule, FormsModule,
MatInputModule, MatInputModule,
@@ -31,4 +31,4 @@ import { PasswordIamPolicyComponent } from './password-iam-policy.component';
DetailLayoutModule, DetailLayoutModule,
], ],
}) })
export class PasswordIamPolicyModule { } export class LabelPolicyModule { }

View File

@@ -6,7 +6,7 @@
<div mat-dialog-content> <div mat-dialog-content>
<mat-form-field *ngIf="serviceType == PolicyComponentServiceType.MGMT" class="full-width" appearance="outline"> <mat-form-field *ngIf="serviceType == PolicyComponentServiceType.MGMT" class="full-width" appearance="outline">
<mat-label>{{ 'IDP.TYPE' | translate }}</mat-label> <mat-label>{{ 'IDP.TYPE' | translate }}</mat-label>
<mat-select [(ngModel)]="idpType" (selectionChange)="loadIdps()" (selectionChange)="loadIdps()"> <mat-select [(ngModel)]="idpType" (selectionChange)="loadIdps()">
<mat-option *ngFor="let type of idpTypes" [value]="type"> <mat-option *ngFor="let type of idpTypes" [value]="type">
{{ 'IDP.TYPES.'+type | translate}} {{ 'IDP.TYPES.'+type | translate}}
</mat-option> </mat-option>

View File

@@ -3,7 +3,7 @@
} }
.desc { .desc {
color: #8795a1; color: var(--grey);
font-size: .9rem; font-size: .9rem;
} }
@@ -18,8 +18,4 @@
.ok-button { .ok-button {
margin-left: .5rem; margin-left: .5rem;
} }
button {
border-radius: .5rem;
}
} }

View File

@@ -61,7 +61,9 @@ export class AddIdpDialogComponent {
query.setKey(IdpSearchKey.IDPSEARCHKEY_PROVIDER_TYPE); query.setKey(IdpSearchKey.IDPSEARCHKEY_PROVIDER_TYPE);
query.setMethod(SearchMethod.SEARCHMETHOD_EQUALS); query.setMethod(SearchMethod.SEARCHMETHOD_EQUALS);
query.setValue(this.idpType.toString()); query.setValue(this.idpType.toString());
this.mgmtService.SearchIdps().then(idps => { console.log(this.idpType);
console.log(query.toObject());
this.mgmtService.SearchIdps(undefined, undefined, [query]).then(idps => {
this.availableIdps = idps.toObject().resultList; this.availableIdps = idps.toObject().resultList;
}); });
} else if (this.serviceType === PolicyComponentServiceType.ADMIN) { } else if (this.serviceType === PolicyComponentServiceType.ADMIN) {

View File

@@ -1,60 +1,76 @@
<app-detail-layout [backRouterLink]="backroutes" [title]="'ORG.POLICY.LOGIN_POLICY.TITLECREATE' | translate" <app-detail-layout [backRouterLink]="[ serviceType === PolicyComponentServiceType.ADMIN ? '/iam' : '/org']"
[description]="'ORG.POLICY.LOGIN_POLICY.DESCRIPTIONCREATE' | translate"> [title]="'POLICY.LOGIN_POLICY.TITLE' | translate"
<ng-container *ngIf="(['policy.delete'] | hasRole | async) && serviceType == PolicyComponentServiceType.MGMT"> [description]="(serviceType==PolicyComponentServiceType.MGMT ? 'POLICY.LOGIN_POLICY.DESCRIPTIONCREATEMGMT' : PolicyComponentServiceType.ADMIN ? 'POLICY.LOGIN_POLICY.DESCRIPTIONCREATEADMIN' : '') | translate">
<button matTooltip="{{'ORG.POLICY.DELETE' | translate}}" color="warn" (click)="deletePolicy()" <p class="default" *ngIf="isDefault"> {{'POLICY.DEFAULTLABEL' | translate}}</p>
mat-stroked-button>
{{'ORG.POLICY.DELETE' | translate}} <div class="spinner-wr">
</button> <mat-spinner diameter="30" *ngIf="loading" color="primary"></mat-spinner>
</div>
<ng-container *ngIf="serviceType === PolicyComponentServiceType.MGMT">
<ng-template appHasRole [appHasRole]="['policy.delete']">
<button *ngIf="!isDefault" matTooltip="{{'POLICY.RESET' | translate}}" color="warn" (click)="removePolicy()"
mat-stroked-button>
{{'POLICY.RESET' | translate}}
</button>
</ng-template>
<ng-template appHasRole [appHasRole]="['policy.write']">
<button *ngIf="isDefault" matTooltip="{{'POLICY.CREATECUSTOM' | translate}}" (click)="savePolicy()"
mat-raised-button>
{{'POLICY.CREATECUSTOM' | translate}}
</button>
</ng-template>
</ng-container> </ng-container>
<div class="content" *ngIf="loginData"> <div class="content" *ngIf="loginData">
<div class="row"> <div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.ALLOWUSERNAMEPASSWORD' | translate}}</span> <mat-slide-toggle class="toggle" color="primary" [disabled]="disabled" ngDefaultControl
<span class="fill-space"></span>
<mat-slide-toggle color="primary" name="hasNumber" ngDefaultControl
[(ngModel)]="loginData.allowUsernamePassword"> [(ngModel)]="loginData.allowUsernamePassword">
{{'POLICY.DATA.ALLOWUSERNAMEPASSWORD' | translate}}
</mat-slide-toggle> </mat-slide-toggle>
<p>{{'POLICY.DATA.ALLOWUSERNAMEPASSWORD_DESC' | translate}}</p>
</div> </div>
<div class="row"> <div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.ALLOWREGISTER' | translate}}</span> <mat-slide-toggle class="toggle" color="primary" [disabled]="disabled" ngDefaultControl
<span class="fill-space"></span> [(ngModel)]="loginData.allowRegister">
<mat-slide-toggle color="primary" name="hasNumber" ngDefaultControl [(ngModel)]="loginData.allowRegister"> {{'POLICY.DATA.ALLOWREGISTER' | translate}}
</mat-slide-toggle> </mat-slide-toggle>
<p> {{'POLICY.DATA.ALLOWREGISTER_DESC' | translate}} </p>
</div> </div>
<div class="row"> <div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.ALLOWEXTERNALIDP' | translate}}</span> <mat-slide-toggle class="toggle" color="primary" [disabled]="disabled" ngDefaultControl
<span class="fill-space"></span>
<mat-slide-toggle color="primary" name="hasNumber" ngDefaultControl
[(ngModel)]="loginData.allowExternalIdp"> [(ngModel)]="loginData.allowExternalIdp">
{{'POLICY.DATA.ALLOWEXTERNALIDP' | translate}}
</mat-slide-toggle> </mat-slide-toggle>
<p> {{'POLICY.DATA.ALLOWEXTERNALIDP_DESC' | translate}} </p>
</div> </div>
</div> </div>
<p class="subheader">{{'LOGINPOLICY.IDPS' | translate}}</p> <h3 class="subheader">{{'LOGINPOLICY.IDPS' | translate}}</h3>
<div class="idps"> <div class="idps">
<div class="idp" *ngFor="let idp of idps"> <div class="idp" [ngClass]="{'disabled': disabled}" *ngFor="let idp of idps">
<mat-icon (click)="removeIdp(idp)" class="rm">remove_circle</mat-icon> <button [disabled]="disabled" mat-icon-button (click)="removeIdp(idp)" class="rm">
<span>{{idp.name}}</span> <mat-icon matTooltip="{{'ACTIONS.REMOVE' | translate}}">
remove_circle</mat-icon>
</button>
<span class="name">{{idp.name}}</span>
<span class="meta">{{ 'IDP.TYPE' | translate }}: {{ 'IDP.TYPES.'+idp.type | translate }}</span> <span class="meta">{{ 'IDP.TYPE' | translate }}: {{ 'IDP.TYPES.'+idp.type | translate }}</span>
<span class="meta">{{ 'IDP.ID' | translate }}: {{idp.idpConfigId}}</span> <span class="meta">{{ 'IDP.ID' | translate }}: {{idp.idpConfigId}}</span>
</div> </div>
<div class="new-idp" (click)="openDialog()"> <div *ngIf="!disabled" class="new-idp" (click)="openDialog()">
<mat-icon>add</mat-icon> <mat-icon>add</mat-icon>
</div> </div>
</div> </div>
<div class="btn-container"> <button [disabled]="disabled" class="save-button" (click)="savePolicy()" color="primary" type="submit"
<button (click)="savePolicy()" color="primary" type="submit" mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
</div>
<ng-template appHasRole [appHasRole]="['org.idp.read']"> <ng-template appHasRole [appHasRole]="['org.idp.read']">
<app-card title="{{ 'IDP.LIST.TITLE' | translate }}" description="{{ 'IDP.LIST.DESCRIPTION' | translate }}"> <h2>{{ 'IDP.LIST.TITLE' | translate }}</h2>
<p>{{ 'IDP.LIST.DESCRIPTION' | translate }}</p>
<app-idp-table [service]="service" [serviceType]="serviceType" <app-idp-table [service]="service" [serviceType]="serviceType"
[disabled]="(['iam.idp.write'] | hasRole | async) == false"> [disabled]="([serviceType == PolicyComponentServiceType.ADMIN ? 'iam.idp.write' : serviceType == PolicyComponentServiceType.MGMT ? 'org.idp.write' : ''] | hasRole | async) == false">
</app-idp-table> </app-idp-table>
</app-card>
</ng-template> </ng-template>
</app-detail-layout> </app-detail-layout>

View File

@@ -1,46 +1,36 @@
.default {
color: #5282c1;
margin-top: 0;
}
button { .spinner-wr {
border-radius: .5rem; margin: .5rem 0;
} }
.content { .content {
padding-top: 1rem; padding-top: 1rem;
display: flex;
flex-direction: column;
width: 100%;
.row { .row {
display: flex; .toggle {
align-items: center; margin: .3rem 0;
padding: .5rem 0;
.left-desc {
color: #8795a1;
font-size: .9rem;
} }
.fill-space { p {
flex: 1; margin-top: .5rem;
} font-size: 14px;
color: var(--grey);
.length-wrapper {
display: flex;
align-items: center;
} }
} }
} }
.btn-container { .save-button {
display: flex; margin-top: 3rem;
justify-content: flex-end; display: block;
width: 100%; padding: .5rem 4rem;
}
button { .idp-table-card {
margin-top: 3rem; width: 100%;
display: block;
padding: .5rem 4rem;
border-radius: .5rem;
}
} }
.subheader { .subheader {
@@ -58,10 +48,16 @@ button {
justify-content: center; justify-content: center;
margin: .5rem; margin: .5rem;
padding: 10px; padding: 10px;
border: 1px solid #8795a1; border: 1px solid var(--grey);
border-radius: .5rem; border-radius: .5rem;
cursor: pointer; cursor: pointer;
position: relative; position: relative;
min-height: 70px;
min-width: 150px;
.name {
font-weight: 700;
}
span { span {
padding: 2px; padding: 2px;
@@ -69,6 +65,7 @@ button {
.meta { .meta {
font-size: 12px; font-size: 12px;
color: var(--grey);
} }
.rm { .rm {
@@ -77,10 +74,16 @@ button {
left: 0; left: 0;
transform: translateX(-50%) translateY(-50%); transform: translateX(-50%) translateY(-50%);
cursor: pointer; cursor: pointer;
&[disabled] {
display: none;
}
} }
&:hover { &:not(.disabled) {
background-color: #ffffff10; &:hover {
background-color: #ffffff10;
}
} }
img { img {

View File

@@ -1,7 +1,6 @@
import { Component, Injector, OnDestroy, Type } from '@angular/core'; import { Component, Injector, OnDestroy, Type } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { Empty } from 'google-protobuf/google/protobuf/empty_pb';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { switchMap } from 'rxjs/operators'; import { switchMap } from 'rxjs/operators';
import { import {
@@ -11,11 +10,11 @@ import {
IdpView as AdminIdpView, IdpView as AdminIdpView,
} from 'src/app/proto/generated/admin_pb'; } from 'src/app/proto/generated/admin_pb';
import { import {
IdpProviderType, IdpProviderType,
IdpProviderView as MgmtIdpProviderView, IdpProviderView as MgmtIdpProviderView,
IdpView as MgmtIdpView, IdpView as MgmtIdpView,
LoginPolicy, LoginPolicy,
LoginPolicyView, OrgDomainView, LoginPolicyView,
} from 'src/app/proto/generated/management_pb'; } from 'src/app/proto/generated/management_pb';
import { AdminService } from 'src/app/services/admin.service'; import { AdminService } from 'src/app/services/admin.service';
import { ManagementService } from 'src/app/services/mgmt.service'; import { ManagementService } from 'src/app/services/mgmt.service';
@@ -34,18 +33,19 @@ export class LoginPolicyComponent implements OnDestroy {
private sub: Subscription = new Subscription(); private sub: Subscription = new Subscription();
public service!: ManagementService | AdminService; public service!: ManagementService | AdminService;
PolicyComponentServiceType: any = PolicyComponentServiceType; public PolicyComponentServiceType: any = PolicyComponentServiceType;
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT; public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
public idps: MgmtIdpProviderView.AsObject[] | AdminIdpProviderView.AsObject[] = []; public idps: MgmtIdpProviderView.AsObject[] | AdminIdpProviderView.AsObject[] = [];
public loading: boolean = false;
public disabled: boolean = true;
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router,
private toast: ToastService, private toast: ToastService,
private dialog: MatDialog, private dialog: MatDialog,
private injector: Injector, private injector: Injector,
) { ) {
this.sub = this.route.data.pipe(switchMap(data => { this.sub = this.route.data.pipe(switchMap(data => {
console.log(data.serviceType);
this.serviceType = data.serviceType; this.serviceType = data.serviceType;
switch (this.serviceType) { switch (this.serviceType) {
case PolicyComponentServiceType.MGMT: case PolicyComponentServiceType.MGMT:
@@ -58,15 +58,20 @@ export class LoginPolicyComponent implements OnDestroy {
return this.route.params; return this.route.params;
})).subscribe(() => { })).subscribe(() => {
this.getData().then(data => { this.fetchData();
if (data) { });
this.loginData = data.toObject(); }
}
}); private fetchData(): void {
this.getIdps().then(idps => { this.getData().then(data => {
console.log(idps); if (data) {
this.idps = idps; this.loginData = data.toObject();
}); this.loading = false;
this.disabled = ((this.loginData as LoginPolicyView.AsObject)?.pb_default) ?? false;
}
});
this.getIdps().then(idps => {
this.idps = idps;
}); });
} }
@@ -107,7 +112,11 @@ export class LoginPolicyComponent implements OnDestroy {
mgmtreq.setAllowExternalIdp(this.loginData.allowExternalIdp); mgmtreq.setAllowExternalIdp(this.loginData.allowExternalIdp);
mgmtreq.setAllowRegister(this.loginData.allowRegister); mgmtreq.setAllowRegister(this.loginData.allowRegister);
mgmtreq.setAllowUsernamePassword(this.loginData.allowUsernamePassword); mgmtreq.setAllowUsernamePassword(this.loginData.allowUsernamePassword);
return (this.service as ManagementService).UpdateLoginPolicy(mgmtreq); if ((this.loginData as LoginPolicyView.AsObject).pb_default) {
return (this.service as ManagementService).CreateLoginPolicy(mgmtreq);
} else {
return (this.service as ManagementService).UpdateLoginPolicy(mgmtreq);
}
case PolicyComponentServiceType.ADMIN: case PolicyComponentServiceType.ADMIN:
const adminreq = new DefaultLoginPolicy(); const adminreq = new DefaultLoginPolicy();
adminreq.setAllowExternalIdp(this.loginData.allowExternalIdp); adminreq.setAllowExternalIdp(this.loginData.allowExternalIdp);
@@ -119,25 +128,27 @@ export class LoginPolicyComponent implements OnDestroy {
public savePolicy(): void { public savePolicy(): void {
this.updateData().then(() => { this.updateData().then(() => {
switch (this.serviceType) { this.toast.showInfo('POLICY.LOGIN_POLICY.SAVED', true);
case PolicyComponentServiceType.MGMT: this.loading = true;
this.router.navigate(['org']); setTimeout(() => {
break; this.fetchData();
case PolicyComponentServiceType.ADMIN: }, 2000);
this.router.navigate(['iam']);
break;
}
}).catch(error => { }).catch(error => {
this.toast.showError(error); this.toast.showError(error);
}); });
} }
public deletePolicy(): Promise<Empty> { public removePolicy(): void {
switch (this.serviceType) { if (this.serviceType === PolicyComponentServiceType.MGMT) {
case PolicyComponentServiceType.MGMT: (this.service as ManagementService).RemoveLoginPolicy().then(() => {
return (this.service as ManagementService).RemoveLoginPolicy(); this.toast.showInfo('POLICY.TOAST.RESETSUCCESS', true);
case PolicyComponentServiceType.ADMIN: this.loading = true;
return (this.service as AdminService).GetDefaultLoginPolicy(); setTimeout(() => {
this.fetchData();
}, 2000);
}).catch(error => {
this.toast.showError(error);
});
} }
} }
@@ -151,7 +162,14 @@ export class LoginPolicyComponent implements OnDestroy {
dialogRef.afterClosed().subscribe(resp => { dialogRef.afterClosed().subscribe(resp => {
if (resp && resp.idp && resp.type) { if (resp && resp.idp && resp.type) {
this.addIdp(resp.idp, resp.type); this.addIdp(resp.idp, resp.type).then(() => {
this.loading = true;
setTimeout(() => {
this.fetchData();
}, 2000);
}).catch(error => {
this.toast.showError(error);
});
} }
}); });
} }
@@ -169,23 +187,29 @@ export class LoginPolicyComponent implements OnDestroy {
public removeIdp(idp: AdminIdpProviderView.AsObject | MgmtIdpProviderView.AsObject): void { public removeIdp(idp: AdminIdpProviderView.AsObject | MgmtIdpProviderView.AsObject): void {
switch (this.serviceType) { switch (this.serviceType) {
case PolicyComponentServiceType.MGMT: case PolicyComponentServiceType.MGMT:
(this.service as ManagementService).RemoveIdpProviderFromLoginPolicy(idp.idpConfigId); (this.service as ManagementService).RemoveIdpProviderFromLoginPolicy(idp.idpConfigId).then(() => {
const index = this.idps.findIndex(temp => temp === idp);
if (index > -1) {
this.idps.splice(index, 1);
}
});
break; break;
case PolicyComponentServiceType.ADMIN: case PolicyComponentServiceType.ADMIN:
(this.service as AdminService).RemoveIdpProviderFromDefaultLoginPolicy(idp.idpConfigId); (this.service as AdminService).RemoveIdpProviderFromDefaultLoginPolicy(idp.idpConfigId).then(() => {
const index = this.idps.findIndex(temp => temp === idp);
if (index > -1) {
this.idps.splice(index, 1);
}
});
break; break;
} }
} }
public get backroutes(): string[] { public get isDefault(): boolean {
switch (this.serviceType) { if (this.loginData && this.serviceType === PolicyComponentServiceType.MGMT) {
case PolicyComponentServiceType.MGMT: return (this.loginData as LoginPolicyView.AsObject).pb_default;
return ['/org']; } else {
case PolicyComponentServiceType.ADMIN: return false;
return ['/iam']; }
break;
} }
return [];
}
} }

View File

@@ -2,21 +2,22 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { CardModule } from 'src/app/modules/card/card.module';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatTooltipModule } from '@angular/material/tooltip'; import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { CardModule } from 'src/app/modules/card/card.module';
import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.module'; import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe.module'; import { IdpTableModule } from 'src/app/modules/idp-table/idp-table.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { AddIdpDialogModule } from './add-idp-dialog/add-idp-dialog.module'; import { AddIdpDialogModule } from './add-idp-dialog/add-idp-dialog.module';
import { LoginPolicyRoutingModule } from './login-policy-routing.module'; import { LoginPolicyRoutingModule } from './login-policy-routing.module';
import { LoginPolicyComponent } from './login-policy.component'; import { LoginPolicyComponent } from './login-policy.component';
import { IdpTableModule } from 'src/app/modules/idp-table/idp-table.module';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
@NgModule({ @NgModule({
declarations: [LoginPolicyComponent], declarations: [LoginPolicyComponent],
@@ -37,6 +38,7 @@ import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
DetailLayoutModule, DetailLayoutModule,
AddIdpDialogModule, AddIdpDialogModule,
IdpTableModule, IdpTableModule,
MatProgressSpinnerModule,
], ],
}) })
export class LoginPolicyModule { } export class LoginPolicyModule { }

View File

@@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { OrgIamPolicyComponent } from './org-iam-policy.component';
const routes: Routes = [
{
path: '',
component: OrgIamPolicyComponent,
data: {
animation: 'DetailPage',
},
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class OrgIamPolicyRoutingModule { }

View File

@@ -0,0 +1,26 @@
<app-detail-layout [backRouterLink]="[ serviceType === PolicyComponentServiceType.ADMIN ? '/iam' : '/org']"
[title]="'POLICY.IAM_POLICY.TITLECREATE' | translate" [description]="'POLICY.IAM_POLICY.DESCRIPTION' | translate">
<p class="default" *ngIf="isDefault"> {{'POLICY.DEFAULTLABEL' | translate}}</p>
<ng-template appHasRole [appHasRole]="['iam.policy.delete']">
<button *ngIf="serviceType === PolicyComponentServiceType.MGMT && !isDefault"
matTooltip="{{'POLICY.RESET' | translate}}" color="warn" (click)="removePolicy()" mat-stroked-button>
{{'POLICY.RESET' | translate}}
</button>
</ng-template>
<div class="content" *ngIf="iamData">
<div class="row">
<span class="left-desc">{{'POLICY.DATA.USERLOGINMUSTBEDOMAIN' | translate}}</span>
<span class="fill-space"></span>
<mat-slide-toggle color="primary" name="hasNumber" ngDefaultControl
[(ngModel)]="iamData.userLoginMustBeDomain">
</mat-slide-toggle>
</div>
</div>
<div class="btn-container">
<button (click)="savePolicy()" color="primary" type="submit"
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
</div>
</app-detail-layout>

View File

@@ -1,6 +1,6 @@
.default {
button { color: #5282c1;
border-radius: .5rem; margin-top: 0;
} }
.content { .content {
@@ -12,10 +12,9 @@ button {
.row { .row {
display: flex; display: flex;
align-items: center; align-items: center;
padding: .5rem 0; padding: .3rem 0;
.left-desc { .left-desc {
color: #8795a1;
font-size: .9rem; font-size: .9rem;
} }
@@ -39,6 +38,5 @@ button {
margin-top: 3rem; margin-top: 3rem;
display: block; display: block;
padding: .5rem 4rem; padding: .5rem 4rem;
border-radius: .5rem;
} }
} }

View File

@@ -1,20 +1,20 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { PasswordIamPolicyComponent } from './password-iam-policy.component'; import { OrgIamPolicyComponent } from './org-iam-policy.component';
describe('PasswordIamPolicyComponent', () => { describe('OrgIamPolicyComponent', () => {
let component: PasswordIamPolicyComponent; let component: OrgIamPolicyComponent;
let fixture: ComponentFixture<PasswordIamPolicyComponent>; let fixture: ComponentFixture<OrgIamPolicyComponent>;
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [PasswordIamPolicyComponent], declarations: [OrgIamPolicyComponent],
}) })
.compileComponents(); .compileComponents();
})); }));
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(PasswordIamPolicyComponent); fixture = TestBed.createComponent(OrgIamPolicyComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@@ -0,0 +1,136 @@
import { Component, Injector, Input, OnDestroy, Type } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { OrgIamPolicyView as AdminOrgIamPolicyView } from 'src/app/proto/generated/admin_pb';
import { Org } from 'src/app/proto/generated/auth_pb';
import { OrgIamPolicyView as MgmtOrgIamPolicyView } from 'src/app/proto/generated/management_pb';
import { AdminService } from 'src/app/services/admin.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { StorageService } from 'src/app/services/storage.service';
import { ToastService } from 'src/app/services/toast.service';
import { PolicyComponentServiceType } from '../policy-component-types.enum';
@Component({
selector: 'app-org-iam-policy',
templateUrl: './org-iam-policy.component.html',
styleUrls: ['./org-iam-policy.component.scss'],
})
export class OrgIamPolicyComponent implements OnDestroy {
@Input() service!: AdminService;
private managementService!: ManagementService;
public serviceType!: PolicyComponentServiceType;
public iamData!: AdminOrgIamPolicyView.AsObject | MgmtOrgIamPolicyView.AsObject;
private sub: Subscription = new Subscription();
private org!: Org.AsObject;
public PolicyComponentServiceType: any = PolicyComponentServiceType;
constructor(
private route: ActivatedRoute,
private toast: ToastService,
private sessionStorage: StorageService,
private injector: Injector,
private adminService: AdminService,
) {
const temporg = this.sessionStorage.getItem('organization') as Org.AsObject;
if (temporg) {
this.org = temporg;
}
this.sub = this.route.data.pipe(switchMap(data => {
this.serviceType = data.serviceType;
if (this.serviceType === PolicyComponentServiceType.MGMT) {
this.managementService = this.injector.get(ManagementService as Type<ManagementService>);
}
return this.route.params;
})).subscribe(_ => {
this.fetchData();
});
}
public ngOnDestroy(): void {
this.sub.unsubscribe();
}
public fetchData(): void {
this.getData().then(data => {
if (data) {
this.iamData = data.toObject();
}
});
}
private async getData(): Promise<AdminOrgIamPolicyView | MgmtOrgIamPolicyView | undefined> {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
return this.managementService.GetMyOrgIamPolicy();
case PolicyComponentServiceType.ADMIN:
if (this.org?.id) {
return this.adminService.GetOrgIamPolicy(this.org.id);
}
break;
}
}
public savePolicy(): void {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
if ((this.iamData as MgmtOrgIamPolicyView.AsObject).pb_default) {
this.adminService.CreateOrgIamPolicy(
this.org.id,
this.iamData.userLoginMustBeDomain,
).then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
}).catch(error => {
this.toast.showError(error);
});
break;
} else {
this.adminService.UpdateOrgIamPolicy(
this.org.id,
this.iamData.userLoginMustBeDomain,
).then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
}).catch(error => {
this.toast.showError(error);
});
break;
}
case PolicyComponentServiceType.ADMIN:
// update Default org iam policy?
this.adminService.UpdateOrgIamPolicy(
this.org.id,
this.iamData.userLoginMustBeDomain,
).then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
}).catch(error => {
this.toast.showError(error);
});
break;
}
}
public removePolicy(): void {
if (this.serviceType === PolicyComponentServiceType.MGMT) {
this.adminService.RemoveOrgIamPolicy(this.org.id).then(() => {
this.toast.showInfo('POLICY.TOAST.RESETSUCCESS', true);
setTimeout(() => {
this.fetchData();
}, 1000);
}).catch(error => {
this.toast.showError(error);
});
}
}
public get isDefault(): boolean {
if (this.iamData && this.serviceType === PolicyComponentServiceType.MGMT) {
return (this.iamData as MgmtOrgIamPolicyView.AsObject).pb_default;
} else {
return false;
}
}
}

View File

@@ -0,0 +1,34 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.module';
import { OrgIamPolicyRoutingModule } from './org-iam-policy-routing.module';
import { OrgIamPolicyComponent } from './org-iam-policy.component';
@NgModule({
declarations: [OrgIamPolicyComponent],
imports: [
OrgIamPolicyRoutingModule,
CommonModule,
FormsModule,
MatInputModule,
MatFormFieldModule,
MatButtonModule,
MatSlideToggleModule,
MatIconModule,
HasRoleModule,
MatTooltipModule,
TranslateModule,
DetailLayoutModule,
],
})
export class OrgIamPolicyModule { }

View File

@@ -1,7 +1,6 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { PolicyComponentAction } from '../policy-component-action.enum';
import { PasswordAgePolicyComponent } from './password-age-policy.component'; import { PasswordAgePolicyComponent } from './password-age-policy.component';
const routes: Routes = [ const routes: Routes = [
@@ -10,15 +9,6 @@ const routes: Routes = [
component: PasswordAgePolicyComponent, component: PasswordAgePolicyComponent,
data: { data: {
animation: 'DetailPage', animation: 'DetailPage',
action: PolicyComponentAction.MODIFY,
},
},
{
path: 'create',
component: PasswordAgePolicyComponent,
data: {
animation: 'DetailPage',
action: PolicyComponentAction.CREATE,
}, },
}, },
]; ];

View File

@@ -1,19 +1,15 @@
<app-detail-layout [backRouterLink]="[ '/org']" [title]="title ? (title | translate) : ''" <app-detail-layout [backRouterLink]="[ serviceType === PolicyComponentServiceType.ADMIN ? '/iam' : '/org']"
[description]="desc ? (desc | translate) : ''"> [title]="'POLICY.PWD_AGE.TITLE' | translate" [description]="'POLICY.PWD_AGE.DESCRIPTION' | translate">
<ng-template appHasRole [appHasRole]="['iam.policy.write']"> <ng-template appHasRole [appHasRole]="['policy.delete']">
<button matTooltip="{{'ORG.POLICY.DELETE' | translate}}" color="warn" (click)="deletePolicy()" <button *ngIf="serviceType === PolicyComponentServiceType.MGMT && !isDefault"
mat-stroked-button> matTooltip="{{'POLICY.RESET' | translate}}" color="warn" (click)="removePolicy()" mat-stroked-button>
{{'ORG.POLICY.DELETE' | translate}} {{'POLICY.RESET' | translate}}
</button> </button>
</ng-template> </ng-template>
<div class="content" *ngIf="ageData"> <div class="content" *ngIf="ageData">
<mat-form-field class="description-formfield" appearance="outline">
<mat-label>{{ 'ORG.POLICY.DATA.DESCRIPTION' | translate }}</mat-label>
<input matInput name="description" ngDefaultControl [(ngModel)]="ageData.description" required />
</mat-form-field>
<div class="row"> <div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.EXPIREWARNDAYS' | translate}}</span> <span class="left-desc">{{'POLICY.DATA.EXPIREWARNDAYS' | translate}}</span>
<span class="fill-space"></span> <span class="fill-space"></span>
<div class="length-wrapper"> <div class="length-wrapper">
<button mat-icon-button (click)="incrementExpireWarnDays()"> <button mat-icon-button (click)="incrementExpireWarnDays()">
@@ -27,7 +23,7 @@
</div> </div>
<div class="row"> <div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.MAXAGEDAYS' | translate}}</span> <span class="left-desc">{{'POLICY.DATA.MAXAGEDAYS' | translate}}</span>
<span class="fill-space"></span> <span class="fill-space"></span>
<div class="length-wrapper"> <div class="length-wrapper">
<button mat-icon-button (click)="incrementMaxAgeDays()"> <button mat-icon-button (click)="incrementMaxAgeDays()">

View File

@@ -1,8 +1,3 @@
button {
border-radius: .5rem;
}
.content { .content {
padding-top: 1rem; padding-top: 1rem;
display: flex; display: flex;
@@ -12,10 +7,10 @@ button {
.row { .row {
display: flex; display: flex;
align-items: center; align-items: center;
padding: .5rem 0; padding: .3rem 0;
.left-desc { .left-desc {
color: #8795a1; color: var(--grey);
font-size: .9rem; font-size: .9rem;
} }
@@ -26,6 +21,7 @@ button {
.length-wrapper { .length-wrapper {
display: flex; display: flex;
align-items: center; align-items: center;
margin-right: -.6rem;
} }
} }
} }
@@ -39,6 +35,5 @@ button {
margin-top: 3rem; margin-top: 3rem;
display: block; display: block;
padding: .5rem 4rem; padding: .5rem 4rem;
border-radius: .5rem;
} }
} }

View File

@@ -1,17 +1,14 @@
import { Component, OnDestroy } from '@angular/core'; import { Component, Injector, OnDestroy, Type } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { switchMap } from 'rxjs/operators'; import { switchMap } from 'rxjs/operators';
import { import { DefaultPasswordAgePolicyView } from 'src/app/proto/generated/admin_pb';
OrgIamPolicy, import { PasswordAgePolicyView } from 'src/app/proto/generated/management_pb';
PasswordAgePolicy, import { AdminService } from 'src/app/services/admin.service';
PasswordComplexityPolicy,
PasswordLockoutPolicy,
} from 'src/app/proto/generated/management_pb';
import { ManagementService } from 'src/app/services/mgmt.service'; import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
import { PolicyComponentAction } from '../policy-component-action.enum'; import { PolicyComponentServiceType } from '../policy-component-types.enum';
@Component({ @Component({
@@ -20,37 +17,37 @@ import { PolicyComponentAction } from '../policy-component-action.enum';
styleUrls: ['./password-age-policy.component.scss'], styleUrls: ['./password-age-policy.component.scss'],
}) })
export class PasswordAgePolicyComponent implements OnDestroy { export class PasswordAgePolicyComponent implements OnDestroy {
public title: string = ''; public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
public desc: string = ''; public service!: AdminService | ManagementService;
componentAction: PolicyComponentAction = PolicyComponentAction.CREATE; public ageData!: PasswordAgePolicyView.AsObject | DefaultPasswordAgePolicyView.AsObject;
public PolicyComponentAction: any = PolicyComponentAction;
public ageData!: PasswordAgePolicy.AsObject;
private sub: Subscription = new Subscription(); private sub: Subscription = new Subscription();
public PolicyComponentServiceType: any = PolicyComponentServiceType;
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private mgmtService: ManagementService,
private router: Router,
private toast: ToastService, private toast: ToastService,
private injector: Injector,
) { ) {
this.sub = this.route.data.pipe(switchMap(data => { this.sub = this.route.data.pipe(switchMap(data => {
this.componentAction = data.action; this.serviceType = data.serviceType;
return this.route.params; switch (this.serviceType) {
})).subscribe(params => { case PolicyComponentServiceType.MGMT:
this.title = 'ORG.POLICY.PWD_AGE.TITLECREATE'; this.service = this.injector.get(ManagementService as Type<ManagementService>);
this.desc = 'ORG.POLICY.PWD_AGE.DESCRIPTIONCREATE'; break;
case PolicyComponentServiceType.ADMIN:
if (this.componentAction === PolicyComponentAction.MODIFY) { this.service = this.injector.get(AdminService as Type<AdminService>);
this.getData(params).then(data => { break;
if (data) {
this.ageData = data.toObject() as PasswordAgePolicy.AsObject;
}
});
} }
return this.route.params;
})).subscribe(() => {
this.getData().then(data => {
if (data) {
this.ageData = data.toObject();
}
});
}); });
} }
@@ -58,19 +55,28 @@ export class PasswordAgePolicyComponent implements OnDestroy {
this.sub.unsubscribe(); this.sub.unsubscribe();
} }
private async getData(params: any): private async getData():
Promise<PasswordLockoutPolicy | PasswordAgePolicy | PasswordComplexityPolicy | OrgIamPolicy | undefined> { Promise<PasswordAgePolicyView | DefaultPasswordAgePolicyView> {
this.title = 'ORG.POLICY.PWD_AGE.TITLE';
this.desc = 'ORG.POLICY.PWD_AGE.DESCRIPTION'; switch (this.serviceType) {
return this.mgmtService.GetPasswordAgePolicy(); case PolicyComponentServiceType.MGMT:
return (this.service as ManagementService).GetPasswordAgePolicy();
case PolicyComponentServiceType.ADMIN:
return (this.service as AdminService).GetDefaultPasswordAgePolicy();
}
} }
public deletePolicy(): void { public removePolicy(): void {
this.mgmtService.DeletePasswordAgePolicy(this.ageData.id).then(() => { if (this.serviceType === PolicyComponentServiceType.MGMT) {
this.toast.showInfo('Successfully deleted'); (this.service as ManagementService).RemovePasswordAgePolicy().then(() => {
}).catch(error => { this.toast.showInfo('POLICY.TOAST.RESETSUCCESS', true);
this.toast.showError(error); setTimeout(() => {
}); this.getData();
}, 1000);
}).catch(error => {
this.toast.showError(error);
});
}
} }
public incrementExpireWarnDays(): void { public incrementExpireWarnDays(): void {
@@ -98,29 +104,46 @@ export class PasswordAgePolicyComponent implements OnDestroy {
} }
public savePolicy(): void { public savePolicy(): void {
if (this.componentAction === PolicyComponentAction.CREATE) { switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
if ((this.ageData as PasswordAgePolicyView.AsObject).pb_default) {
(this.service as ManagementService).CreatePasswordAgePolicy(
this.ageData.maxAgeDays,
this.ageData.expireWarnDays,
).then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
}).catch(error => {
this.toast.showError(error);
});
} else {
(this.service as ManagementService).UpdatePasswordAgePolicy(
this.ageData.maxAgeDays,
this.ageData.expireWarnDays,
).then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
}).catch(error => {
this.toast.showError(error);
});
}
break;
case PolicyComponentServiceType.ADMIN:
(this.service as AdminService).UpdateDefaultPasswordAgePolicy(
this.ageData.maxAgeDays,
this.ageData.expireWarnDays,
).then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
}).catch(error => {
this.toast.showError(error);
});
break;
}
}
this.mgmtService.CreatePasswordAgePolicy( public get isDefault(): boolean {
this.ageData.description, if (this.ageData && this.serviceType === PolicyComponentServiceType.MGMT) {
this.ageData.maxAgeDays, return (this.ageData as PasswordAgePolicyView.AsObject).pb_default;
this.ageData.expireWarnDays, } else {
).then(() => { return false;
this.router.navigate(['org']);
}).catch(error => {
this.toast.showError(error);
});
} else if (this.componentAction === PolicyComponentAction.MODIFY) {
this.mgmtService.UpdatePasswordAgePolicy(
this.ageData.description,
this.ageData.maxAgeDays,
this.ageData.expireWarnDays,
).then(() => {
this.router.navigate(['org']);
}).catch(error => {
this.toast.showError(error);
});
} }
} }
} }

View File

@@ -1,7 +1,6 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { PolicyComponentAction } from '../policy-component-action.enum';
import { PasswordComplexityPolicyComponent } from './password-complexity-policy.component'; import { PasswordComplexityPolicyComponent } from './password-complexity-policy.component';
const routes: Routes = [ const routes: Routes = [
@@ -10,15 +9,6 @@ const routes: Routes = [
component: PasswordComplexityPolicyComponent, component: PasswordComplexityPolicyComponent,
data: { data: {
animation: 'DetailPage', animation: 'DetailPage',
action: PolicyComponentAction.MODIFY,
},
},
{
path: 'create',
component: PasswordComplexityPolicyComponent,
data: {
animation: 'DetailPage',
action: PolicyComponentAction.CREATE,
}, },
}, },
]; ];

View File

@@ -1,19 +1,22 @@
<app-detail-layout [backRouterLink]="[ '/org']" [title]="title ? (title | translate) : ''" <app-detail-layout [backRouterLink]="[ serviceType === PolicyComponentServiceType.ADMIN ? '/iam' : '/org']"
[description]="desc ? (desc | translate) : ''"> [title]="'POLICY.PWD_COMPLEXITY.TITLE' | translate" [description]="'POLICY.PWD_COMPLEXITY.DESCRIPTION' | translate">
<ng-template appHasRole [appHasRole]="['iam.policy.write']">
<button matTooltip="{{'ORG.POLICY.DELETE' | translate}}" color="warn" (click)="deletePolicy()" <p class="default" *ngIf="isDefault"> {{'POLICY.DEFAULTLABEL' | translate}}</p>
mat-stroked-button>
{{'ORG.POLICY.DELETE' | translate}} <div class="spinner-wr">
<mat-spinner diameter="30" *ngIf="loading" color="primary"></mat-spinner>
</div>
<ng-template appHasRole [appHasRole]="['policy.delete']">
<button *ngIf="serviceType === PolicyComponentServiceType.MGMT && !isDefault"
matTooltip="{{'POLICY.RESET' | translate}}" color="warn" (click)="removePolicy()" mat-stroked-button>
{{'POLICY.RESET' | translate}}
</button> </button>
</ng-template> </ng-template>
<div *ngIf="complexityData" class="content"> <div *ngIf="complexityData" class="content">
<mat-form-field class="description-formfield" appearance="outline">
<mat-label>{{ 'ORG.POLICY.DATA.DESCRIPTION' | translate }}</mat-label>
<input matInput name="description" ngDefaultControl [(ngModel)]="complexityData.description" required />
</mat-form-field>
<div class="row"> <div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.MINLENGTH' | translate}}</span> <span class="left-desc">{{'POLICY.DATA.MINLENGTH' | translate}}</span>
<span class="fill-space"></span> <span class="fill-space"></span>
<div class="length-wrapper"> <div class="length-wrapper">
<button mat-icon-button (click)="decrementLength()"> <button mat-icon-button (click)="decrementLength()">
@@ -26,26 +29,26 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.HASNUMBER' | translate}}</span> <span class="left-desc">{{'POLICY.DATA.HASNUMBER' | translate}}</span>
<span class="fill-space"></span> <span class="fill-space"></span>
<mat-slide-toggle color="primary" name="hasNumber" ngDefaultControl [(ngModel)]="complexityData.hasNumber"> <mat-slide-toggle color="primary" name="hasNumber" ngDefaultControl [(ngModel)]="complexityData.hasNumber">
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>
<div class="row"> <div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.HASSYMBOL' | translate}}</span> <span class="left-desc">{{'POLICY.DATA.HASSYMBOL' | translate}}</span>
<span class="fill-space"></span> <span class="fill-space"></span>
<mat-slide-toggle color="primary" name="hasSymbol" ngDefaultControl [(ngModel)]="complexityData.hasSymbol"> <mat-slide-toggle color="primary" name="hasSymbol" ngDefaultControl [(ngModel)]="complexityData.hasSymbol">
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>
<div class="row"> <div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.HASLOWERCASE' | translate}}</span> <span class="left-desc">{{'POLICY.DATA.HASLOWERCASE' | translate}}</span>
<span class="fill-space"></span> <span class="fill-space"></span>
<mat-slide-toggle color="primary" name="hasLowercase" ngDefaultControl <mat-slide-toggle color="primary" name="hasLowercase" ngDefaultControl
[(ngModel)]="complexityData.hasLowercase"> [(ngModel)]="complexityData.hasLowercase">
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>
<div class="row"> <div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.HASUPPERCASE' | translate}}</span> <span class="left-desc">{{'POLICY.DATA.HASUPPERCASE' | translate}}</span>
<span class="fill-space"></span> <span class="fill-space"></span>
<mat-slide-toggle color="primary" name="hasUppercase" ngDefaultControl <mat-slide-toggle color="primary" name="hasUppercase" ngDefaultControl
[(ngModel)]="complexityData.hasUppercase"> [(ngModel)]="complexityData.hasUppercase">
@@ -54,7 +57,7 @@
</div> </div>
<div class="btn-container"> <div class="btn-container">
<button (click)="savePolicy()" color="primary" type="submit" [disabled]="!complexityData?.description" <button (click)="savePolicy()" color="primary" type="submit"
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button> mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
</div> </div>
</app-detail-layout> </app-detail-layout>

View File

@@ -1,6 +1,10 @@
.default {
color: #5282c1;
margin-top: 0;
}
button { .spinner-wr {
border-radius: .5rem; margin: .5rem 0;
} }
.content { .content {
@@ -12,10 +16,9 @@ button {
.row { .row {
display: flex; display: flex;
align-items: center; align-items: center;
padding: .5rem 0; padding: .3rem 0;
.left-desc { .left-desc {
color: #8795a1;
font-size: .9rem; font-size: .9rem;
} }
@@ -26,6 +29,7 @@ button {
.length-wrapper { .length-wrapper {
display: flex; display: flex;
align-items: center; align-items: center;
margin-right: -.6rem;
} }
} }
} }
@@ -39,6 +43,5 @@ button {
margin-top: 3rem; margin-top: 3rem;
display: block; display: block;
padding: .5rem 4rem; padding: .5rem 4rem;
border-radius: .5rem;
} }
} }

View File

@@ -1,17 +1,14 @@
import { Component, OnDestroy } from '@angular/core'; import { Component, Injector, OnDestroy, Type } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { switchMap } from 'rxjs/operators'; import { switchMap } from 'rxjs/operators';
import { import { DefaultPasswordComplexityPolicy } from 'src/app/proto/generated/admin_pb';
OrgIamPolicy, import { PasswordComplexityPolicyView } from 'src/app/proto/generated/management_pb';
PasswordAgePolicy, import { AdminService } from 'src/app/services/admin.service';
PasswordComplexityPolicy,
PasswordLockoutPolicy,
} from 'src/app/proto/generated/management_pb';
import { ManagementService } from 'src/app/services/mgmt.service'; import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
import { PolicyComponentAction } from '../policy-component-action.enum'; import { PolicyComponentServiceType } from '../policy-component-types.enum';
@Component({ @Component({
selector: 'app-password-policy', selector: 'app-password-policy',
@@ -19,36 +16,45 @@ import { PolicyComponentAction } from '../policy-component-action.enum';
styleUrls: ['./password-complexity-policy.component.scss'], styleUrls: ['./password-complexity-policy.component.scss'],
}) })
export class PasswordComplexityPolicyComponent implements OnDestroy { export class PasswordComplexityPolicyComponent implements OnDestroy {
public title: string = ''; public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
public desc: string = ''; public service!: ManagementService | AdminService;
componentAction: PolicyComponentAction = PolicyComponentAction.CREATE; public complexityData!: PasswordComplexityPolicyView.AsObject | DefaultPasswordComplexityPolicy.AsObject;
public PolicyComponentAction: any = PolicyComponentAction;
public complexityData!: PasswordComplexityPolicy.AsObject;
private sub: Subscription = new Subscription(); private sub: Subscription = new Subscription();
public PolicyComponentServiceType: any = PolicyComponentServiceType;
public loading: boolean = false;
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private mgmtService: ManagementService,
private router: Router,
private toast: ToastService, private toast: ToastService,
private injector: Injector,
) { ) {
this.sub = this.route.data.pipe(switchMap(data => { this.sub = this.route.data.pipe(switchMap(data => {
this.componentAction = data.action; this.serviceType = data.serviceType;
return this.route.params;
})).subscribe(params => {
this.title = 'ORG.POLICY.PWD_COMPLEXITY.TITLECREATE';
this.desc = 'ORG.POLICY.PWD_COMPLEXITY.DESCRIPTIONCREATE';
if (this.componentAction === PolicyComponentAction.MODIFY) { switch (this.serviceType) {
this.getData(params).then(data => { case PolicyComponentServiceType.MGMT:
if (data) { this.service = this.injector.get(ManagementService as Type<ManagementService>);
this.complexityData = data.toObject() as PasswordComplexityPolicy.AsObject; break;
} case PolicyComponentServiceType.ADMIN:
}); this.service = this.injector.get(AdminService as Type<AdminService>);
break;
}
return this.route.params;
})).subscribe(() => {
this.fetchData();
});
}
public fetchData(): void {
this.loading = true;
this.getData().then(data => {
if (data) {
this.complexityData = data.toObject();
this.loading = false;
} }
}); });
} }
@@ -57,19 +63,27 @@ export class PasswordComplexityPolicyComponent implements OnDestroy {
this.sub.unsubscribe(); this.sub.unsubscribe();
} }
private async getData(params: any): private async getData():
Promise<PasswordLockoutPolicy | PasswordAgePolicy | PasswordComplexityPolicy | OrgIamPolicy | undefined> { Promise<PasswordComplexityPolicyView | DefaultPasswordComplexityPolicy> {
this.title = 'ORG.POLICY.PWD_COMPLEXITY.TITLE'; switch (this.serviceType) {
this.desc = 'ORG.POLICY.PWD_COMPLEXITY.DESCRIPTION'; case PolicyComponentServiceType.MGMT:
return this.mgmtService.GetPasswordComplexityPolicy(); return (this.service as ManagementService).GetPasswordComplexityPolicy();
case PolicyComponentServiceType.ADMIN:
return (this.service as AdminService).GetDefaultPasswordComplexityPolicy();
}
} }
public deletePolicy(): void { public removePolicy(): void {
this.mgmtService.DeletePasswordComplexityPolicy(this.complexityData.id).then(() => { if (this.service instanceof ManagementService) {
this.toast.showInfo('Successfully deleted'); this.service.removePasswordComplexityPolicy().then(() => {
}).catch(error => { this.toast.showInfo('POLICY.TOAST.RESETSUCCESS', true);
this.toast.showError(error); setTimeout(() => {
}); this.fetchData();
}, 1000);
}).catch(error => {
this.toast.showError(error);
});
}
} }
public incrementLength(): void { public incrementLength(): void {
@@ -85,35 +99,56 @@ export class PasswordComplexityPolicyComponent implements OnDestroy {
} }
public savePolicy(): void { public savePolicy(): void {
if (this.componentAction === PolicyComponentAction.CREATE) { switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
if ((this.complexityData as PasswordComplexityPolicyView.AsObject).pb_default) {
(this.service as ManagementService).CreatePasswordComplexityPolicy(
this.mgmtService.CreatePasswordComplexityPolicy( this.complexityData.hasLowercase,
this.complexityData.description, this.complexityData.hasUppercase,
this.complexityData.hasLowercase, this.complexityData.hasNumber,
this.complexityData.hasUppercase, this.complexityData.hasSymbol,
this.complexityData.hasNumber, this.complexityData.minLength,
this.complexityData.hasSymbol, ).then(() => {
this.complexityData.minLength, this.toast.showInfo('POLICY.TOAST.SET', true);
).then(() => { }).catch(error => {
this.router.navigate(['org']); this.toast.showError(error);
}).catch(error => { });
this.toast.showError(error); } else {
}); (this.service as ManagementService).UpdatePasswordComplexityPolicy(
this.complexityData.hasLowercase,
this.complexityData.hasUppercase,
this.complexityData.hasNumber,
this.complexityData.hasSymbol,
this.complexityData.minLength,
).then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
}).catch(error => {
this.toast.showError(error);
});
}
break;
case PolicyComponentServiceType.ADMIN:
(this.service as AdminService).UpdateDefaultPasswordComplexityPolicy(
this.complexityData.hasLowercase,
this.complexityData.hasUppercase,
this.complexityData.hasNumber,
this.complexityData.hasSymbol,
this.complexityData.minLength,
).then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
}).catch(error => {
this.toast.showError(error);
});
break;
}
}
} else if (this.componentAction === PolicyComponentAction.MODIFY) { public get isDefault(): boolean {
if (this.complexityData && this.serviceType === PolicyComponentServiceType.MGMT) {
this.mgmtService.UpdatePasswordComplexityPolicy( return (this.complexityData as PasswordComplexityPolicyView.AsObject).pb_default;
this.complexityData.description, } else {
this.complexityData.hasLowercase, return false;
this.complexityData.hasUppercase,
this.complexityData.hasNumber,
this.complexityData.hasSymbol,
this.complexityData.minLength,
).then(() => {
this.router.navigate(['org']);
}).catch(error => {
this.toast.showError(error);
});
} }
} }
} }

View File

@@ -5,6 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatTooltipModule } from '@angular/material/tooltip'; import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
@@ -29,6 +30,7 @@ import { PasswordComplexityPolicyComponent } from './password-complexity-policy.
MatTooltipModule, MatTooltipModule,
TranslateModule, TranslateModule,
DetailLayoutModule, DetailLayoutModule,
MatProgressSpinnerModule,
], ],
}) })
export class PasswordComplexityPolicyModule { } export class PasswordComplexityPolicyModule { }

View File

@@ -1,30 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { PolicyComponentAction } from '../policy-component-action.enum';
import { PasswordIamPolicyComponent } from './password-iam-policy.component';
const routes: Routes = [
{
path: '',
component: PasswordIamPolicyComponent,
data: {
animation: 'DetailPage',
action: PolicyComponentAction.MODIFY,
},
},
{
path: 'create',
component: PasswordIamPolicyComponent,
data: {
animation: 'DetailPage',
action: PolicyComponentAction.CREATE,
},
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class PasswordIamPolicyRoutingModule { }

View File

@@ -1,28 +0,0 @@
<app-detail-layout [backRouterLink]="[ '/org']" [title]="title ? (title | translate) : ''"
[description]="desc ? (desc | translate) : ''">
<!-- <ng-template appHasRole [appHasRole]="['iam.policy.write']">
<button matTooltip="{{'ORG.POLICY.DELETE' | translate}}" color="warn" (click)="deletePolicy()"
mat-stroked-button>
{{'ORG.POLICY.DELETE' | translate}}
</button>
</ng-template> -->
<div class="content" *ngIf="iamData">
<mat-form-field class="description-formfield" appearance="outline">
<mat-label>{{ 'ORG.POLICY.DATA.DESCRIPTION' | translate }}</mat-label>
<input matInput name="description" ngDefaultControl [(ngModel)]="iamData.description" required />
</mat-form-field>
<div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.USERLOGINMUSTBEDOMAIN' | translate}}</span>
<span class="fill-space"></span>
<mat-slide-toggle color="primary" name="hasNumber" ngDefaultControl
[(ngModel)]="iamData.userLoginMustBeDomain">
</mat-slide-toggle>
</div>
</div>
<div class="btn-container">
<button (click)="savePolicy()" color="primary" type="submit" [disabled]="!iamData?.description"
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
</div>
</app-detail-layout>

View File

@@ -1,102 +0,0 @@
import { Component, OnDestroy } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import {
OrgIamPolicy,
PasswordAgePolicy,
PasswordComplexityPolicy,
PasswordLockoutPolicy,
} from 'src/app/proto/generated/management_pb';
import { AdminService } from 'src/app/services/admin.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { StorageService } from 'src/app/services/storage.service';
import { ToastService } from 'src/app/services/toast.service';
import { PolicyComponentAction } from '../policy-component-action.enum';
@Component({
selector: 'app-password-iam-policy',
templateUrl: './password-iam-policy.component.html',
styleUrls: ['./password-iam-policy.component.scss'],
})
export class PasswordIamPolicyComponent implements OnDestroy {
public title: string = '';
public desc: string = '';
componentAction: PolicyComponentAction = PolicyComponentAction.CREATE;
public PolicyComponentAction: any = PolicyComponentAction;
public iamData!: OrgIamPolicy.AsObject;
private sub: Subscription = new Subscription();
constructor(
private route: ActivatedRoute,
private mgmtService: ManagementService,
private adminService: AdminService,
private router: Router,
private toast: ToastService,
private sessionStorage: StorageService,
) {
this.sub = this.route.data.pipe(switchMap(data => {
this.componentAction = data.action;
console.log(data.action);
return this.route.params;
})).subscribe(params => {
this.title = 'ORG.POLICY.IAM_POLICY.TITLECREATE';
this.desc = 'ORG.POLICY.IAM_POLICY.DESCRIPTIONCREATE';
if (this.componentAction === PolicyComponentAction.MODIFY) {
this.getData(params).then(data => {
if (data) {
this.iamData = data.toObject() as OrgIamPolicy.AsObject;
}
});
}
});
}
public ngOnDestroy(): void {
this.sub.unsubscribe();
}
private async getData(params: any):
Promise<PasswordLockoutPolicy | PasswordAgePolicy | PasswordComplexityPolicy | OrgIamPolicy | undefined> {
this.title = 'ORG.POLICY.IAM_POLICY.TITLECREATE';
this.desc = 'ORG.POLICY.IAM_POLICY.DESCRIPTIONCREATE';
return this.mgmtService.GetMyOrgIamPolicy();
}
public savePolicy(): void {
if (this.componentAction === PolicyComponentAction.CREATE) {
const orgId = this.sessionStorage.getItem('organization');
if (orgId) {
this.adminService.CreateOrgIamPolicy(
orgId,
this.iamData.description,
this.iamData.userLoginMustBeDomain,
).then(() => {
this.router.navigate(['org']);
}).catch(error => {
this.toast.showError(error);
});
}
} else if (this.componentAction === PolicyComponentAction.MODIFY) {
const orgId = this.sessionStorage.getItem('organization');
if (orgId) {
this.adminService.UpdateOrgIamPolicy(
orgId,
this.iamData.description,
this.iamData.userLoginMustBeDomain,
).then(() => {
this.router.navigate(['org']);
}).catch(error => {
this.toast.showError(error);
});
}
}
}
}

View File

@@ -1,7 +1,6 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { PolicyComponentAction } from '../policy-component-action.enum';
import { PasswordLockoutPolicyComponent } from './password-lockout-policy.component'; import { PasswordLockoutPolicyComponent } from './password-lockout-policy.component';
const routes: Routes = [ const routes: Routes = [
@@ -10,15 +9,6 @@ const routes: Routes = [
component: PasswordLockoutPolicyComponent, component: PasswordLockoutPolicyComponent,
data: { data: {
animation: 'DetailPage', animation: 'DetailPage',
action: PolicyComponentAction.MODIFY,
},
},
{
path: 'create',
component: PasswordLockoutPolicyComponent,
data: {
animation: 'DetailPage',
action: PolicyComponentAction.CREATE,
}, },
}, },
]; ];

View File

@@ -1,19 +1,17 @@
<app-detail-layout [backRouterLink]="[ '/org']" [title]="title ? (title | translate) : ''" <app-detail-layout [backRouterLink]="[ serviceType === PolicyComponentServiceType.ADMIN ? '/iam' : '/org']"
[description]="desc ? (desc | translate) : ''"> [title]="'POLICY.PWD_LOCKOUT.TITLE' | translate" [description]="'POLICY.PWD_LOCKOUT.DESCRIPTION' | translate">
<ng-template appHasRole [appHasRole]="['iam.policy.write']"> <p class="default" *ngIf="isDefault"> {{'POLICY.DEFAULTLABEL' | translate}}</p>
<button matTooltip="{{'ORG.POLICY.DELETE' | translate}}" color="warn" (click)="deletePolicy()"
mat-stroked-button> <ng-template appHasRole [appHasRole]="['policy.delete']">
{{'ORG.POLICY.DELETE' | translate}} <button *ngIf="serviceType === PolicyComponentServiceType.MGMT && !isDefault"
matTooltip="{{'POLICY.RESET' | translate}}" color="warn" (click)="removePolicy()" mat-stroked-button>
{{'POLICY.RESET' | translate}}
</button> </button>
</ng-template> </ng-template>
<div class="content" *ngIf="lockoutData"> <div class="content" *ngIf="lockoutData">
<mat-form-field class="description-formfield" appearance="outline">
<mat-label>{{ 'ORG.POLICY.DATA.DESCRIPTION' | translate }}</mat-label>
<input matInput name="description" ngDefaultControl [(ngModel)]="lockoutData.description" required />
</mat-form-field>
<div class="row"> <div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.MAXATTEMPTS' | translate}}</span> <span class="left-desc">{{'POLICY.DATA.MAXATTEMPTS' | translate}}</span>
<span class="fill-space"></span> <span class="fill-space"></span>
<div class="length-wrapper"> <div class="length-wrapper">
<button mat-icon-button (click)="incrementMaxAttempts()"> <button mat-icon-button (click)="incrementMaxAttempts()">
@@ -26,10 +24,10 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<span class="left-desc">{{'ORG.POLICY.DATA.SHOWLOCKOUTFAILURES' | translate}}</span> <span class="left-desc">{{'POLICY.DATA.SHOWLOCKOUTFAILURES' | translate}}</span>
<span class="fill-space"></span> <span class="fill-space"></span>
<mat-slide-toggle color="primary" name="showLockOutFailures" ngDefaultControl <mat-slide-toggle color="primary" name="showLockoutFailure" ngDefaultControl
[(ngModel)]="lockoutData.showLockOutFailures"> [(ngModel)]="lockoutData.showLockoutFailure">
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>
</div> </div>

View File

@@ -1,6 +1,6 @@
.default {
button { color: #5282c1;
border-radius: .5rem; margin-top: 0;
} }
.content { .content {
@@ -12,10 +12,9 @@ button {
.row { .row {
display: flex; display: flex;
align-items: center; align-items: center;
padding: .5rem 0; padding: .3rem 0;
.left-desc { .left-desc {
color: #8795a1;
font-size: .9rem; font-size: .9rem;
} }
@@ -39,6 +38,5 @@ button {
margin-top: 3rem; margin-top: 3rem;
display: block; display: block;
padding: .5rem 4rem; padding: .5rem 4rem;
border-radius: .5rem;
} }
} }

View File

@@ -1,18 +1,15 @@
import { Component, OnDestroy } from '@angular/core'; import { Component, Injector, Input, OnDestroy, Type } from '@angular/core';
import { FormGroup } from '@angular/forms'; import { FormGroup } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { switchMap } from 'rxjs/operators'; import { switchMap } from 'rxjs/operators';
import { import { DefaultPasswordLockoutPolicyView } from 'src/app/proto/generated/admin_pb';
OrgIamPolicy, import { PasswordLockoutPolicyView } from 'src/app/proto/generated/management_pb';
PasswordAgePolicy, import { AdminService } from 'src/app/services/admin.service';
PasswordComplexityPolicy,
PasswordLockoutPolicy,
} from 'src/app/proto/generated/management_pb';
import { ManagementService } from 'src/app/services/mgmt.service'; import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
import { PolicyComponentAction } from '../policy-component-action.enum'; import { PolicyComponentServiceType } from '../policy-component-types.enum';
@Component({ @Component({
selector: 'app-password-lockout-policy', selector: 'app-password-lockout-policy',
@@ -20,37 +17,35 @@ import { PolicyComponentAction } from '../policy-component-action.enum';
styleUrls: ['./password-lockout-policy.component.scss'], styleUrls: ['./password-lockout-policy.component.scss'],
}) })
export class PasswordLockoutPolicyComponent implements OnDestroy { export class PasswordLockoutPolicyComponent implements OnDestroy {
public title: string = ''; @Input() public service!: ManagementService | AdminService;
public desc: string = ''; public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
componentAction: PolicyComponentAction = PolicyComponentAction.CREATE;
public PolicyComponentAction: any = PolicyComponentAction;
public lockoutForm!: FormGroup; public lockoutForm!: FormGroup;
public lockoutData!: PasswordLockoutPolicy.AsObject; public lockoutData!: PasswordLockoutPolicyView.AsObject;
private sub: Subscription = new Subscription(); private sub: Subscription = new Subscription();
public PolicyComponentServiceType: any = PolicyComponentServiceType;
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private mgmtService: ManagementService,
private router: Router,
private toast: ToastService, private toast: ToastService,
private injector: Injector,
) { ) {
this.sub = this.route.data.pipe(switchMap(data => { this.sub = this.route.data.pipe(switchMap(data => {
this.componentAction = data.action; this.serviceType = data.serviceType;
return this.route.params;
})).subscribe(params => {
this.title = 'ORG.POLICY.PWD_LOCKOUT.TITLECREATE';
this.desc = 'ORG.POLICY.PWD_LOCKOUT.DESCRIPTIONCREATE';
if (this.componentAction === PolicyComponentAction.MODIFY) { switch (this.serviceType) {
this.getData(params).then(data => { case PolicyComponentServiceType.MGMT:
if (data) { this.service = this.injector.get(ManagementService as Type<ManagementService>);
this.lockoutData = data.toObject() as PasswordLockoutPolicy.AsObject; break;
} case PolicyComponentServiceType.ADMIN:
}); this.service = this.injector.get(AdminService as Type<AdminService>);
break;
} }
return this.route.params;
})).subscribe(() => {
this.fetchData();
}); });
} }
@@ -58,20 +53,32 @@ export class PasswordLockoutPolicyComponent implements OnDestroy {
this.sub.unsubscribe(); this.sub.unsubscribe();
} }
private async getData(params: any): private fetchData(): void {
Promise<PasswordLockoutPolicy | PasswordAgePolicy | PasswordComplexityPolicy | OrgIamPolicy | undefined> { this.getData().then(data => {
if (data) {
this.title = 'ORG.POLICY.PWD_LOCKOUT.TITLE'; this.lockoutData = data.toObject() as PasswordLockoutPolicyView.AsObject;
this.desc = 'ORG.POLICY.PWD_LOCKOUT.DESCRIPTION'; }
return this.mgmtService.GetPasswordLockoutPolicy(); });
} }
public deletePolicy(): void { private getData(): Promise<PasswordLockoutPolicyView | DefaultPasswordLockoutPolicyView> {
this.mgmtService.DeletePasswordLockoutPolicy(this.lockoutData.id).then(() => { switch (this.serviceType) {
this.toast.showInfo('Successfully deleted'); case PolicyComponentServiceType.MGMT:
}).catch(error => { return (this.service as ManagementService).GetPasswordLockoutPolicy();
this.toast.showError(error); case PolicyComponentServiceType.ADMIN:
}); return (this.service as AdminService).GetDefaultPasswordLockoutPolicy();
}
}
public removePolicy(): void {
if (this.service instanceof ManagementService) {
this.service.RemovePasswordLockoutPolicy().then(() => {
this.toast.showInfo('POLICY.TOAST.RESETSUCCESS', true);
this.fetchData();
}).catch(error => {
this.toast.showError(error);
});
}
} }
public incrementMaxAttempts(): void { public incrementMaxAttempts(): void {
@@ -87,27 +94,44 @@ export class PasswordLockoutPolicyComponent implements OnDestroy {
} }
public savePolicy(): void { public savePolicy(): void {
if (this.componentAction === PolicyComponentAction.CREATE) { let promise: Promise<any>;
this.mgmtService.CreatePasswordLockoutPolicy( if (this.service instanceof AdminService) {
this.lockoutData.description, promise = this.service.UpdateDefaultPasswordLockoutPolicy(
this.lockoutData.maxAttempts, this.lockoutData.maxAttempts,
this.lockoutData.showLockOutFailures, this.lockoutData.showLockoutFailure,
).then(() => { ).then(() => {
this.router.navigate(['org']); this.toast.showInfo('POLICY.TOAST.SET', true);
}).catch(error => { }).catch(error => {
this.toast.showError(error); this.toast.showError(error);
}); });
} else if (this.componentAction === PolicyComponentAction.MODIFY) { } else {
if ((this.lockoutData as PasswordLockoutPolicyView.AsObject).pb_default) {
promise = this.service.CreatePasswordLockoutPolicy(
this.lockoutData.maxAttempts,
this.lockoutData.showLockoutFailure,
).then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
}).catch(error => {
this.toast.showError(error);
});
} else {
promise = this.service.UpdatePasswordLockoutPolicy(
this.lockoutData.maxAttempts,
this.lockoutData.showLockoutFailure,
).then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
}).catch(error => {
this.toast.showError(error);
});
}
}
}
this.mgmtService.UpdatePasswordLockoutPolicy( public get isDefault(): boolean {
this.lockoutData.description, if (this.lockoutData && this.serviceType === PolicyComponentServiceType.MGMT) {
this.lockoutData.maxAttempts, return (this.lockoutData as PasswordLockoutPolicyView.AsObject).pb_default;
this.lockoutData.showLockOutFailures, } else {
).then(() => { return false;
this.router.navigate(['org']);
}).catch(error => {
this.toast.showError(error);
});
} }
} }
} }

View File

@@ -1,5 +0,0 @@
export enum PolicyComponentAction {
CREATE = 'create',
MODIFY = 'modify',
}

View File

@@ -4,6 +4,7 @@ export enum PolicyComponentType {
COMPLEXITY = 'complexity', COMPLEXITY = 'complexity',
IAM = 'iam', IAM = 'iam',
LOGIN = 'login', LOGIN = 'login',
LABEL = 'label',
} }
export enum PolicyComponentServiceType { export enum PolicyComponentServiceType {
MGMT = 'mgmt', MGMT = 'mgmt',

View File

@@ -0,0 +1,109 @@
<h1>{{'POLICY.TITLE' | translate}}</h1>
<p class="top-desc">{{'POLICY.DESCRIPTION' | translate}}</p>
<div class="row-lyt">
<ng-template appHasRole
[appHasRole]="PolicyGridType.IAM ? ['iam.policy.read'] : PolicyGridType.ORG ? ['policy.read'] : []">
<div class="p-item card">
<div class="avatar">
<mat-icon class="icon" svgIcon="mdi_textbox_password"></mat-icon>
</div>
<div class="title">
<span>{{'POLICY.PWD_COMPLEXITY.TITLE' | translate}}</span>
<button mat-icon-button disabled>
<i class="icon las la-check-circle"></i>
</button>
</div>
<p class="desc">
{{'POLICY.PWD_COMPLEXITY.DESCRIPTION' | translate}}</p>
<span class="fill-space"></span>
<div class="btn-wrapper">
<button [routerLink]="[ 'policy', PolicyComponentType.COMPLEXITY ]"
mat-stroked-button>{{'POLICY.BTN_EDIT' | translate}}</button>
</div>
</div>
</ng-template>
<ng-template appHasRole [appHasRole]="['iam.policy.read']">
<div class="p-item card">
<div class="avatar">
<i class="icon las la-gem"></i>
</div>
<div class="title">
<span>{{'POLICY.IAM_POLICY.TITLE' | translate}}</span>
<button mat-icon-button disabled>
<i class="icon las la-check-circle"></i>
</button>
</div>
<p class="desc">
{{'POLICY.IAM_POLICY.DESCRIPTION' | translate}}</p>
<span class="fill-space"></span>
<div class="btn-wrapper">
<ng-template appHasRole [appHasRole]="['iam.policy.write']">
<button [routerLink]="[ 'policy', PolicyComponentType.IAM ]"
mat-stroked-button>{{'POLICY.BTN_EDIT' | translate}}</button>
</ng-template>
</div>
</div>
</ng-template>
<ng-template appHasRole
[appHasRole]="PolicyGridType.IAM ? ['iam.policy.read'] : PolicyGridType.ORG ? ['policy.read'] : []">
<div class="p-item card">
<div class="avatar">
<i class="icon las la-sign-in-alt"></i>
</div>
<div class="title">
<span>{{'POLICY.LOGIN_POLICY.TITLE' | translate}}</span>
<button mat-icon-button disabled>
<i class="icon las la-check-circle"></i>
</button>
</div>
<ng-template #showDescIAM>
<p class="desc">
{{'POLICY.LOGIN_POLICY.DESCRIPTION' | translate}}</p>
</ng-template>
<span class="fill-space"></span>
<div class="btn-wrapper">
<ng-template appHasRole [appHasRole]="['policy.write']">
<button [routerLink]="[ 'policy', PolicyComponentType.LOGIN ]"
mat-stroked-button>{{'POLICY.BTN_EDIT' | translate}}</button>
</ng-template>
</div>
</div>
</ng-template>
<ng-container *ngIf="type === PolicyGridType.IAM">
<ng-template appHasRole [appHasRole]="['iam.policy.read']">
<div class="p-item card">
<div class="avatar">
<i class="icon las la-envelope"></i>
</div>
<div class="title">
<span>{{'POLICY.LABEL.TITLE' | translate}}</span>
<button mat-icon-button disabled>
<i class="icon las la-check-circle"></i>
</button>
</div>
<p class="desc">
{{'POLICY.LABEL.DESCRIPTION' | translate}}</p>
<span class="fill-space"></span>
<div class="btn-wrapper">
<ng-template appHasRole [appHasRole]="['iam.policy.write']">
<button [routerLink]="[ 'policy', PolicyComponentType.LABEL ]"
mat-stroked-button>{{'POLICY.BTN_EDIT' | translate}}</button>
</ng-template>
</div>
</div>
</ng-template>
</ng-container>
</div>

View File

@@ -3,17 +3,17 @@ h1 {
} }
.top-desc { .top-desc {
color: #8795a1; color: var(--grey);
} }
.row-lyt { .row-lyt {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
margin: 0 -1rem; margin: 0 -.5rem;
.p-item { .p-item {
flex-basis: 300px; flex-basis: 290px;
margin: 1rem; margin: .5rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 200px; min-height: 200px;
@@ -38,6 +38,7 @@ h1 {
font-size: 2.5rem; font-size: 2.5rem;
height: 2.5rem; height: 2.5rem;
line-height: 2.5rem; line-height: 2.5rem;
color: white;
} }
} }
@@ -46,7 +47,7 @@ h1 {
align-items: center; align-items: center;
span { span {
font-size: 1.2rem; font-size: 1.1rem;
} }
.icon { .icon {
@@ -56,8 +57,8 @@ h1 {
} }
.desc { .desc {
font-size: .9rem; font-size: 14px;
color: #8795a1; color: var(--grey);
} }
.fill-space { .fill-space {
@@ -70,7 +71,6 @@ h1 {
button { button {
margin-right: 1rem; margin-right: 1rem;
border-radius: .5rem;
} }
} }
} }

View File

@@ -0,0 +1,19 @@
import { Component, Input } from '@angular/core';
import { PolicyComponentType } from 'src/app/modules/policies/policy-component-types.enum';
export enum PolicyGridType {
ORG,
IAM,
}
@Component({
selector: 'app-policy-grid',
templateUrl: './policy-grid.component.html',
styleUrls: ['./policy-grid.component.scss'],
})
export class PolicyGridComponent {
@Input() public type!: PolicyGridType;
public PolicyComponentType: any = PolicyComponentType;
public PolicyGridType: any = PolicyGridType;
constructor() { }
}

View File

@@ -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 { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { PolicyGridComponent } from './policy-grid.component';
@NgModule({
declarations: [PolicyGridComponent],
imports: [
CommonModule,
HasRolePipeModule,
HasRoleModule,
TranslateModule,
RouterModule,
MatButtonModule,
MatIconModule,
],
exports: [
PolicyGridComponent,
],
})
export class PolicyGridModule { }

View File

@@ -1,96 +1,26 @@
<app-detail-layout *ngIf="project" [backRouterLink]="[ '/projects', project?.projectId]" <app-detail-layout *ngIf="project" [backRouterLink]="[ '/projects', project?.projectId]"
title="{{projectName}} {{ 'PROJECT.MEMBER.TITLE' | translate }}" title="{{projectName}} {{ 'PROJECT.MEMBER.TITLE' | translate }}"
description="{{ 'PROJECT.MEMBER.DESCRIPTION' | translate }}"> description="{{ 'PROJECT.MEMBER.DESCRIPTION' | translate }}">
<app-refresh-table *ngIf="project" (refreshed)="changePage()" [dataSize]="dataSource.totalResult" <app-members-table *ngIf="project" [dataSource]="dataSource" [memberRoleOptions]="memberRoleOptions"
[timestamp]="dataSource.viewTimestamp" [selection]="selection" [loading]="dataSource?.loading$ | async"> (updateRoles)="updateRoles($event.member, $event.change)" [factoryLoadFunc]="changePageFactory"
<ng-template appHasRole actions (changedSelection)="selection = $event" [refreshTrigger]="changePage"
[canWrite]="['project.member.write$', 'project.member.write:'+ project.projectId] | hasRole | async"
[canDelete]="['project.member.delete$', 'project.member.delete:'+project.projectId] | hasRole | async"
(deleteMember)="removeProjectMember($event)">
<ng-template appHasRole selectactions
[appHasRole]="['project.member.delete:' + project.projectId, 'project.member.delete']"> [appHasRole]="['project.member.delete:' + project.projectId, 'project.member.delete']">
<button (click)="removeProjectMemberSelection()" color="warn" <button (click)="removeProjectMemberSelection()" color="warn"
matTooltip="{{'ORG_DETAIL.TABLE.DELETE' | translate}}" class="icon-button" mat-icon-button matTooltip="{{'ORG_DETAIL.TABLE.DELETE' | translate}}" class="del-button" mat-raised-button>
*ngIf="selection.hasValue()">
<i class="las la-trash"></i> <i class="las la-trash"></i>
{{'ACTIONS.SELECTIONDELETE' | translate}}
</button> </button>
</ng-template> </ng-template>
<ng-template appHasRole actions <ng-template appHasRole writeactions
[appHasRole]="['project.member.write:'+project.projectId,'project.member.write']"> [appHasRole]="['project.member.write:'+project.projectId,'project.member.write']">
<a color="primary" [disabled]="disabled" class="add-button" (click)="openAddMember()" color="primary" <a color="primary" (click)="openAddMember()" color="primary" mat-raised-button>
mat-raised-button>
<mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }} <mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }}
</a> </a>
</ng-template> </ng-template>
</app-members-table>
<div class="table-wrapper"> </app-detail-layout>
<table mat-table class="table" aria-label="Elements" [dataSource]="dataSource"> <!-- TODO: check for both project.member and project.grant.member permissions -->
<ng-container matColumnDef="select">
<th class="selection" mat-header-cell *matHeaderCellDef>
<mat-checkbox color="primary" (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 color="primary" (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)">
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="userId">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.USERID' | translate }} </th>
<td class="pointer" [routerLink]="['/user', member.userId]" mat-cell *matCellDef="let member">
{{member.userId}} </td>
</ng-container>
<ng-container matColumnDef="firstname">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.FIRSTNAME' | translate }} </th>
<td class="pointer" [routerLink]="['/user', member.userId]" mat-cell *matCellDef="let member">
{{member.firstName}} </td>
</ng-container>
<ng-container matColumnDef="lastname">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.LASTNAME' | translate }} </th>
<td class="pointer" [routerLink]="['/user', member.userId]" mat-cell *matCellDef="let member">
{{member.lastName}} </td>
</ng-container>
<ng-container matColumnDef="username">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.USERNAME' | translate }} </th>
<td class="pointer" [routerLink]="['/user', member.userId]" mat-cell *matCellDef="let member">
{{member.userName}} </td>
</ng-container>
<ng-container matColumnDef="email">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.EMAIL' | translate }} </th>
<td class="pointer" [routerLink]="['/user', member.userId]" mat-cell *matCellDef="let member">
{{member.email}}
</td>
</ng-container>
<ng-container matColumnDef="roles">
<th mat-header-cell *matHeaderCellDef> {{ 'ROLESLABEL' | translate }} </th>
<td mat-cell *matCellDef="let member">
<mat-form-field class="form-field" appearance="outline" *ngIf="project">
<mat-label>{{ 'ROLESLABEL' | translate }}</mat-label>
<mat-select [(ngModel)]="member.rolesList" multiple
[disabled]="([('project.member.write:' + project.projectId), 'project.member.write'] | hasRole | async) == false"
(selectionChange)="updateRoles(member, $event)">
<mat-option *ngFor="let role of memberRoleOptions" [value]="role">
{{ role }}
</mat-option>
</mat-select>
</mat-form-field>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="data-row" mat-row *matRowDef="let row; columns: displayedColumns;">
</tr>
</table>
<mat-paginator *ngIf="dataSource" class="paginator" #paginator [pageSize]="INITIALPAGESIZE"
[length]="dataSource.totalResult" [pageSizeOptions]="[25, 50, 100, 250]" (page)="changePage($event)">
</mat-paginator>
</div>
</app-refresh-table>
</app-detail-layout>

View File

@@ -1,50 +1,7 @@
.icon-button { .del-button {
margin-right: .5rem; margin-right: .5rem;
} }
.add-button { :root {
border-radius: .5rem; width: 100%;
}
.table-wrapper {
overflow-x: auto;
.table,
.paginator {
width: 100%;
td,
th {
padding: .5rem;
&: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;
}
}
}
.pointer {
outline: none;
cursor: pointer;
} }

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