mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 18:07:31 +00:00
Merge branch 'master' into new-eventstore
This commit is contained in:
8
.github/dependabot.yml
vendored
8
.github/dependabot.yml
vendored
@@ -24,3 +24,11 @@ updates:
|
||||
commit-message:
|
||||
prefix: chore
|
||||
include: scope
|
||||
- package-ecosystem: npm
|
||||
directory: "/site"
|
||||
schedule:
|
||||
interval: monthly
|
||||
open-pull-requests-limit: 10
|
||||
commit-message:
|
||||
prefix: chore
|
||||
include: scope
|
||||
|
4
.github/workflows/codeql-analysis.yml
vendored
4
.github/workflows/codeql-analysis.yml
vendored
@@ -3,9 +3,13 @@ name: "Code scanning - action"
|
||||
on:
|
||||
push:
|
||||
branches: [master, ]
|
||||
paths-ignore:
|
||||
- 'site/**'
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [master]
|
||||
paths-ignore:
|
||||
- 'site/**'
|
||||
schedule:
|
||||
- cron: '0 12 * * 2'
|
||||
|
||||
|
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
@@ -47,4 +47,4 @@ jobs:
|
||||
ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
BRANCH: gh-pages
|
||||
FOLDER: site/__sapper__/export
|
||||
CLEAN: true
|
||||
CLEAN: true
|
28
.github/workflows/release.yml
vendored
28
.github/workflows/release.yml
vendored
@@ -5,7 +5,7 @@ env:
|
||||
GITHUB_TOKEN: ${{ secrets.CR_PAT }}
|
||||
REGISTRY: ghcr.io
|
||||
NODE_VERSION: '12'
|
||||
GO_VERSION: '1.14'
|
||||
GO_VERSION: '1.15'
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -126,30 +126,6 @@ jobs:
|
||||
repository: ${{ github.repository }}
|
||||
tag_with_ref: 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:
|
||||
runs-on: ubuntu-18.04
|
||||
@@ -191,4 +167,4 @@ jobs:
|
||||
if: env.CAOS_NEXT_VERSION != ''
|
||||
- name: Docker Push 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
19
.github/workflows/spellcheck.yml
vendored
Normal 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
40
.typo-ci.yml
Normal 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
11
CONTRIBUTING.md
Normal 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).
|
38
README.md
38
README.md
@@ -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" />
|
||||
|
||||
[](https://github.com/semantic-release/semantic-release)
|
||||
[](https://github.com/caos/zitadel/actions)
|
||||
@@ -7,48 +7,49 @@
|
||||
[](https://goreportcard.com/report/github.com/caos/zitadel)
|
||||
[](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
|
||||
|
||||
`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
|
||||
|
||||
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.
|
||||
`ZITADEL` only needs `Kubernetes` for orchestration and `CockroachDB` as storage.
|
||||
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 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**](https://kubernetes.io/) for orchestration and [**CockroachDB**](https://www.cockroachlabs.com/) as storage.
|
||||
|
||||
## 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.
|
||||
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.
|
||||
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.
|
||||
|
||||
## How To Use It
|
||||
|
||||
### Use our free tier
|
||||
|
||||
Stay tuned, we will publish how you can register an organisation in our cloud offering `zitadel.ch` soon.
|
||||
Yes we have a free tier!
|
||||
We provide a shared-cloud ZITADEL system where people can register there own organisation.
|
||||
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
|
||||
|
||||
Stay tuned, we will soon publish a guide how you can deploy a `hyperconverged` system with our automation tooling called `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.
|
||||
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 [**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.
|
||||
|
||||
See our progress [here](https://github.com/caos/orbos/pull/256)
|
||||
The combination of the tools [**ORBOS**](https://github.com/caos/orbos/) and **ZITADEL** is what makes the operation easy and scalable.
|
||||
|
||||
## 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
|
||||
|
||||
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
|
||||
|
||||
@@ -59,3 +60,4 @@ See the policy [here](./SECURITY.md)
|
||||
See the exact licensing terms [here](./LICENSE)
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
|
@@ -1,5 +0,0 @@
|
||||
# Build
|
||||
|
||||
## Console
|
||||
|
||||
## Docker
|
@@ -23,52 +23,75 @@ Eventstore:
|
||||
MaxCacheSizeInByte: 10485760 #10mb
|
||||
|
||||
SetUp:
|
||||
GlobalOrg: 'Global'
|
||||
IAMProject: 'Zitadel'
|
||||
DefaultLoginPolicy:
|
||||
AllowUsernamePassword: true
|
||||
AllowRegister: true
|
||||
AllowExternalIdp: true
|
||||
Orgs:
|
||||
- Name: 'Global'
|
||||
Domain: 'global.caos.ch'
|
||||
Default: true
|
||||
OrgIamPolicy: true
|
||||
Users:
|
||||
- FirstName: 'Global Org'
|
||||
LastName: 'Administrator'
|
||||
UserName: 'zitadel-global-org-admin@caos.ch'
|
||||
Email: 'zitadel-global-org-admin@caos.ch'
|
||||
Password: 'Password1!'
|
||||
Owners:
|
||||
- 'zitadel-global-org-admin@caos.ch'
|
||||
- Name: 'CAOS AG'
|
||||
Domain: 'caos.ch'
|
||||
Users:
|
||||
- FirstName: 'Zitadel'
|
||||
LastName: 'Administrator'
|
||||
UserName: 'zitadel-admin'
|
||||
Email: 'zitadel-admin@caos.ch'
|
||||
Password: 'Password1!'
|
||||
Owners:
|
||||
- 'zitadel-admin@caos.ch'
|
||||
Projects:
|
||||
- Name: 'Zitadel'
|
||||
OIDCApps:
|
||||
- Name: 'Management-API'
|
||||
- Name: 'Auth-API'
|
||||
- Name: 'Admin-API'
|
||||
- Name: 'Zitadel Console'
|
||||
RedirectUris:
|
||||
- '$ZITADEL_CONSOLE/auth/callback'
|
||||
PostLogoutRedirectUris:
|
||||
- '$ZITADEL_CONSOLE/signedout'
|
||||
ResponseTypes:
|
||||
- $ZITADEL_CONSOLE_RESPONSE_TYPE
|
||||
GrantTypes:
|
||||
- $ZITADEL_CONSOLE_GRANT_TYPE
|
||||
ApplicationType: 'USER_AGENT'
|
||||
AuthMethodType: 'NONE'
|
||||
DevMode: $ZITADEL_CONSOLE_DEV_MODE
|
||||
Owners:
|
||||
- 'zitadel-admin@caos.ch'
|
||||
Step1:
|
||||
GlobalOrg: 'Global'
|
||||
IAMProject: 'Zitadel'
|
||||
DefaultLoginPolicy:
|
||||
AllowUsernamePassword: true
|
||||
AllowRegister: true
|
||||
AllowExternalIdp: true
|
||||
Orgs:
|
||||
- Name: 'Global'
|
||||
Domain: 'global.caos.ch'
|
||||
Default: true
|
||||
OrgIamPolicy: true
|
||||
Users:
|
||||
- FirstName: 'Global Org'
|
||||
LastName: 'Administrator'
|
||||
UserName: 'zitadel-global-org-admin@caos.ch'
|
||||
Email: 'zitadel-global-org-admin@caos.ch'
|
||||
Password: 'Password1!'
|
||||
Owners:
|
||||
- 'zitadel-global-org-admin@caos.ch'
|
||||
- Name: 'CAOS AG'
|
||||
Domain: 'caos.ch'
|
||||
Users:
|
||||
- FirstName: 'Zitadel'
|
||||
LastName: 'Administrator'
|
||||
UserName: 'zitadel-admin'
|
||||
Email: 'zitadel-admin@caos.ch'
|
||||
Password: 'Password1!'
|
||||
Owners:
|
||||
- 'zitadel-admin@caos.ch'
|
||||
Projects:
|
||||
- Name: 'Zitadel'
|
||||
OIDCApps:
|
||||
- Name: 'Management-API'
|
||||
- Name: 'Auth-API'
|
||||
- Name: 'Admin-API'
|
||||
- Name: 'Zitadel Console'
|
||||
RedirectUris:
|
||||
- '$ZITADEL_CONSOLE/auth/callback'
|
||||
PostLogoutRedirectUris:
|
||||
- '$ZITADEL_CONSOLE/signedout'
|
||||
ResponseTypes:
|
||||
- $ZITADEL_CONSOLE_RESPONSE_TYPE
|
||||
GrantTypes:
|
||||
- $ZITADEL_CONSOLE_GRANT_TYPE
|
||||
ApplicationType: 'USER_AGENT'
|
||||
AuthMethodType: 'NONE'
|
||||
DevMode: $ZITADEL_CONSOLE_DEV_MODE
|
||||
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'
|
||||
|
@@ -52,28 +52,10 @@ SystemDefaults:
|
||||
EncryptionKeyID: $ZITADEL_OTP_VERIFICATION_KEY
|
||||
VerificationLifetimes:
|
||||
PasswordCheck: 240h #10d
|
||||
ExternalLoginCheck: 240h #10d
|
||||
MfaInitSkip: 720h #30d
|
||||
MfaSoftwareCheck: 18h
|
||||
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'
|
||||
DomainVerification:
|
||||
VerificationKey:
|
||||
|
@@ -26,7 +26,7 @@
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets",
|
||||
"src/manifest.webmanifest",
|
||||
"src/manifest.webmanifest"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
@@ -34,8 +34,9 @@
|
||||
"scripts": [],
|
||||
"allowedCommonJsDependencies": [
|
||||
"@angular/common/locales/de",
|
||||
"src/app/proto/generated/*.js",
|
||||
"src/app/proto/generated/**/*.js"
|
||||
"src/app/proto/generated/**",
|
||||
"file-saver",
|
||||
"qrcode"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
|
3593
console/package-lock.json
generated
3593
console/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -27,7 +27,7 @@
|
||||
"@types/file-saver": "^2.0.1",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@types/google-protobuf": "^3.7.3",
|
||||
"angularx-qrcode": "^10.0.10",
|
||||
"angularx-qrcode": "^10.0.11",
|
||||
"angular-oauth2-oidc": "^10.0.3",
|
||||
"cors": "^2.8.5",
|
||||
"file-saver": "^2.0.2",
|
||||
@@ -35,34 +35,34 @@
|
||||
"google-protobuf": "^3.13.0",
|
||||
"grpc": "^1.24.3",
|
||||
"grpc-web": "^1.2.1",
|
||||
"moment": "^2.27.0",
|
||||
"moment": "^2.29.1",
|
||||
"ngx-moment": "^5.0.0",
|
||||
"ngx-quicklink": "^0.2.4",
|
||||
"rxjs": "~6.6.3",
|
||||
"ts-protoc-gen": "^0.12.0",
|
||||
"tslib": "^2.0.1",
|
||||
"uuid": "^8.3.0",
|
||||
"zone.js": "~0.11.1"
|
||||
"ts-protoc-gen": "^0.13.0",
|
||||
"tslib": "^2.0.3",
|
||||
"uuid": "^8.3.1",
|
||||
"zone.js": "~0.11.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~0.1000.8",
|
||||
"@angular/cli": "~10.0.7",
|
||||
"@angular-devkit/build-angular": "~0.1002.0",
|
||||
"@angular/cli": "~10.2.0",
|
||||
"@angular/compiler-cli": "~10.0.11",
|
||||
"@types/jasmine": "~3.5.13",
|
||||
"@angular/language-service": "~10.1.0",
|
||||
"@types/jasmine": "~3.6.0",
|
||||
"@angular/language-service": "~10.2.0",
|
||||
"@types/jasminewd2": "~2.0.3",
|
||||
"@types/node": "^14.6.4",
|
||||
"codelyzer": "^6.0.0",
|
||||
"@types/node": "^14.14.3",
|
||||
"codelyzer": "^6.0.1",
|
||||
"jasmine-core": "~3.6.0",
|
||||
"jasmine-spec-reporter": "~5.0.0",
|
||||
"karma": "~5.2.1",
|
||||
"jasmine-spec-reporter": "~6.0.0",
|
||||
"karma": "~5.2.3",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-coverage-istanbul-reporter": "~3.0.2",
|
||||
"karma-jasmine": "~4.0.1",
|
||||
"karma-jasmine-html-reporter": "^1.5.0",
|
||||
"prettier": "^2.1.1",
|
||||
"prettier": "^2.1.2",
|
||||
"protractor": "~7.0.0",
|
||||
"stylelint": "^13.7.1",
|
||||
"stylelint": "^13.7.2",
|
||||
"stylelint-config-standard": "^20.0.0",
|
||||
"stylelint-scss": "^3.18.0",
|
||||
"ts-node": "~9.0.0",
|
||||
|
@@ -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', [
|
||||
transition('HomePage => AddPage', [
|
||||
style({ transform: 'translateX(100%)', opacity: 0.5 }),
|
||||
|
@@ -65,6 +65,14 @@ const routes: Routes = [
|
||||
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',
|
||||
canActivate: [AuthGuard],
|
||||
@@ -87,6 +95,24 @@ const routes: Routes = [
|
||||
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'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<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">
|
||||
<button aria-label="Toggle sidenav" mat-icon-button (click)="drawer.toggle()">
|
||||
<i class="icon las la-bars"></i>
|
||||
@@ -12,22 +12,31 @@
|
||||
</ng-template>
|
||||
</a>
|
||||
|
||||
<button (click)="loadOrgs()" *ngIf="profile?.id && org" mat-button
|
||||
[matMenuTriggerFor]="menu">{{org?.name ? org.name : 'NO NAME'}}
|
||||
<button (click)="loadOrgs()" *ngIf="profile?.id && org" mat-button [matMenuTriggerFor]="menu"
|
||||
(menuOpened)="focusFilter()">{{org?.name ? org.name : 'NO NAME'}}
|
||||
<mat-icon>
|
||||
arrow_drop_down</mat-icon>
|
||||
</button>
|
||||
|
||||
<mat-menu #menu="matMenu">
|
||||
<mat-progress-bar *ngIf="orgLoading" color="accent" mode="indeterminate"></mat-progress-bar>
|
||||
<button class="show-all" mat-menu-item
|
||||
[routerLink]="[ '/org/overview' ]">{{'MENU.SHOWORGS' | translate}}</button>
|
||||
<mat-menu class="menu" #menu="matMenu">
|
||||
<div class="spinner-w">
|
||||
<mat-spinner diameter="20" *ngIf="orgLoading$ | async" color="accent">
|
||||
</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"
|
||||
*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'}}
|
||||
</button>
|
||||
|
||||
<button class="show-all" mat-menu-item
|
||||
[routerLink]="[ '/org/overview' ]">{{'MENU.SHOWORGS' | translate}}</button>
|
||||
|
||||
<ng-template appHasRole [appHasRole]="['org.create','iam.write']">
|
||||
<button mat-menu-item [routerLink]="[ '/org/create' ]">
|
||||
<mat-icon class="avatar">add</mat-icon>
|
||||
@@ -129,6 +138,21 @@
|
||||
<span class="label">{{ 'MENU.MACHINEUSERS' | translate }}</span>
|
||||
</a>
|
||||
</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>
|
||||
|
||||
<span class="fill-space"></span>
|
||||
|
@@ -1,3 +1,4 @@
|
||||
@import '~@angular/material/theming';
|
||||
|
||||
.root-header {
|
||||
position: fixed;
|
||||
@@ -159,13 +160,6 @@
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.primary-button {
|
||||
margin: 1rem;
|
||||
border-radius: 1.5rem;
|
||||
height: 2.5rem;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
@@ -174,7 +168,7 @@
|
||||
|
||||
.router {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,18 +208,50 @@
|
||||
margin: .5rem 0;
|
||||
|
||||
span {
|
||||
border: 1px solid #ffffff10;
|
||||
border: 1px solid #81868a40;
|
||||
padding: 2px 1rem;
|
||||
border-radius: 50vw;
|
||||
color: #8795a1;
|
||||
font-size: 12px;
|
||||
color: var(--grey);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.line {
|
||||
display: block;
|
||||
background-color: #ffffff10;
|
||||
background-color: #81868a40;
|
||||
height: 1px;
|
||||
margin: .5rem 0;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@@ -1,17 +1,24 @@
|
||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||
import { OverlayContainer } from '@angular/cdk/overlay';
|
||||
import { ViewportScroller } from '@angular/common';
|
||||
import { Component, HostBinding, Inject, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { DOCUMENT, ViewportScroller } from '@angular/common';
|
||||
import { Component, ElementRef, HostBinding, Inject, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { MatIconRegistry } from '@angular/material/icon';
|
||||
import { MatDrawer } from '@angular/material/sidenav';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { Router, RouterOutlet } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Observable, of, Subscription } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { LangChangeEvent, TranslateService } from '@ngx-translate/core';
|
||||
import { BehaviorSubject, from, Observable, of, Subscription } from 'rxjs';
|
||||
import { catchError, debounceTime, finalize, map, take } from 'rxjs/operators';
|
||||
|
||||
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 { GrpcAuthService } from './services/grpc-auth.service';
|
||||
import { ManagementService } from './services/mgmt.service';
|
||||
@@ -32,6 +39,7 @@ import { UpdateService } from './services/update.service';
|
||||
})
|
||||
export class AppComponent implements OnDestroy {
|
||||
@ViewChild('drawer') public drawer!: MatDrawer;
|
||||
@ViewChild('input', { static: false }) input!: ElementRef;
|
||||
public isHandset$: Observable<boolean> = this.breakpointObserver
|
||||
.observe('(max-width: 599px)')
|
||||
.pipe(map(result => {
|
||||
@@ -41,17 +49,18 @@ export class AppComponent implements OnDestroy {
|
||||
|
||||
public showAccount: boolean = false;
|
||||
public org!: Org.AsObject;
|
||||
public orgs: Org.AsObject[] = [];
|
||||
public orgs$: Observable<Org.AsObject[]> = of([]);
|
||||
public profile!: UserProfileView.AsObject;
|
||||
public isDarkTheme: Observable<boolean> = of(true);
|
||||
|
||||
public orgLoading: boolean = false;
|
||||
public orgLoading$: BehaviorSubject<any> = new BehaviorSubject(false);
|
||||
|
||||
public showProjectSection: boolean = false;
|
||||
|
||||
public grantedProjectsCount: number = 0;
|
||||
public ownedProjectsCount: number = 0;
|
||||
|
||||
public filterControl: FormControl = new FormControl('');
|
||||
private authSub: Subscription = new Subscription();
|
||||
private orgSub: Subscription = new Subscription();
|
||||
|
||||
@@ -70,6 +79,7 @@ export class AppComponent implements OnDestroy {
|
||||
private toast: ToastService,
|
||||
private router: Router,
|
||||
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('%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.org = org;
|
||||
|
||||
this.getProjectCount();
|
||||
});
|
||||
|
||||
@@ -160,6 +169,16 @@ export class AppComponent implements OnDestroy {
|
||||
|
||||
this.isDarkTheme = this.themeService.isDarkTheme;
|
||||
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 {
|
||||
@@ -167,15 +186,26 @@ export class AppComponent implements OnDestroy {
|
||||
this.orgSub.unsubscribe();
|
||||
}
|
||||
|
||||
public loadOrgs(): void {
|
||||
this.orgLoading = true;
|
||||
this.authService.SearchMyProjectOrgs(10, 0).then(res => {
|
||||
this.orgs = res.toObject().resultList;
|
||||
this.orgLoading = false;
|
||||
}).catch(error => {
|
||||
this.toast.showError(error);
|
||||
this.orgLoading = false;
|
||||
});
|
||||
public loadOrgs(filter?: string): void {
|
||||
let query;
|
||||
if (filter) {
|
||||
query = new MyProjectOrgSearchQuery();
|
||||
query.setMethod(SearchMethod.SEARCHMETHOD_CONTAINS_IGNORE_CASE);
|
||||
query.setKey(MyProjectOrgSearchKey.MYPROJECTORGSEARCHKEY_ORG_NAME);
|
||||
query.setValue(filter);
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -200,19 +230,24 @@ export class AppComponent implements OnDestroy {
|
||||
|
||||
this.authService.user.subscribe(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.document.documentElement.lang = lang;
|
||||
});
|
||||
}
|
||||
|
||||
public setActiveOrg(org: Org.AsObject): void {
|
||||
this.org = org;
|
||||
this.authService.setActiveOrg(org);
|
||||
this.router.navigate(['/']);
|
||||
this.authService.zitadelPermissionsChanged.pipe(take(1)).subscribe(() => {
|
||||
this.router.navigate(['/']);
|
||||
});
|
||||
}
|
||||
|
||||
private getProjectCount(): void {
|
||||
this.authService.isAllowed(['project.read']).subscribe((allowed) => {
|
||||
this.authService.isAllowed(['project.read$']).subscribe((allowed) => {
|
||||
if (allowed) {
|
||||
this.mgmtService.SearchProjects(0, 0).then(res => {
|
||||
this.ownedProjectsCount = res.toObject().totalResult;
|
||||
@@ -224,5 +259,11 @@ export class AppComponent implements OnDestroy {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
focusFilter(): void {
|
||||
setTimeout(() => {
|
||||
this.input.nativeElement.focus();
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -3,13 +3,17 @@ import { CommonModule, registerLocaleData } from '@angular/common';
|
||||
import { HttpClient, HttpClientModule } from '@angular/common/http';
|
||||
import localeDe from '@angular/common/locales/de';
|
||||
import { APP_INITIALIZER, NgModule } from '@angular/core';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatNativeDateModule } from '@angular/material/core';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
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 { AuthConfig, OAuthModule, OAuthStorage } from 'angular-oauth2-oidc';
|
||||
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 { 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 { WarnDialogModule } from './modules/warn-dialog/warn-dialog.module';
|
||||
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 { GrpcService } from './services/grpc.service';
|
||||
import { AuthInterceptor } from './services/interceptors/auth.interceptor';
|
||||
import { GRPC_INTERCEPTORS } from './services/interceptors/grpc-interceptor';
|
||||
import { I18nInterceptor } from './services/interceptors/i18n.interceptor';
|
||||
import { OrgInterceptor } from './services/interceptors/org.interceptor';
|
||||
import { RefreshService } from './services/refresh.service';
|
||||
import { SeoService } from './services/seo.service';
|
||||
import { StatehandlerProcessorService, StatehandlerProcessorServiceImpl } from './services/statehandler-processor.service';
|
||||
import { StatehandlerService, StatehandlerServiceImpl } from './services/statehandler.service';
|
||||
import { StorageService } from './services/storage.service';
|
||||
@@ -104,9 +110,13 @@ const authConfig: AuthConfig = {
|
||||
MatSidenavModule,
|
||||
MatCardModule,
|
||||
OutsideClickModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
HasRolePipeModule,
|
||||
MatProgressBarModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatToolbarModule,
|
||||
ReactiveFormsModule,
|
||||
MatMenuModule,
|
||||
MatSnackBarModule,
|
||||
AvatarModule,
|
||||
@@ -150,11 +160,17 @@ const authConfig: AuthConfig = {
|
||||
multi: true,
|
||||
useClass: AuthInterceptor,
|
||||
},
|
||||
{
|
||||
provide: GRPC_INTERCEPTORS,
|
||||
multi: true,
|
||||
useClass: I18nInterceptor,
|
||||
},
|
||||
{
|
||||
provide: GRPC_INTERCEPTORS,
|
||||
multi: true,
|
||||
useClass: OrgInterceptor,
|
||||
},
|
||||
SeoService,
|
||||
RefreshService,
|
||||
GrpcService,
|
||||
GrpcAuthService,
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, first, switchMap } from 'rxjs/operators';
|
||||
|
||||
import { GrpcAuthService } from '../services/grpc-auth.service';
|
||||
|
||||
@@ -15,6 +16,11 @@ export class RoleGuard implements CanActivate {
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot,
|
||||
): 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'])),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
28
console/src/app/guards/user.guard.ts
Normal file
28
console/src/app/guards/user.guard.ts
Normal 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']);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
@@ -16,7 +16,7 @@
|
||||
</app-avatar>
|
||||
|
||||
<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="email">{{'USER.STATE.'+user.authState | translate}}</span>
|
||||
</div>
|
||||
@@ -28,7 +28,7 @@
|
||||
<i class="las la-user-plus"></i>
|
||||
</div>
|
||||
<span class="col">
|
||||
<span class="title">{{'USER.ADDACCOUNT' | translate}}</span>
|
||||
<span class="user-title">{{'USER.ADDACCOUNT' | translate}}</span>
|
||||
</span>
|
||||
<span class="fill-space"></span>
|
||||
<mat-icon>keyboard_arrow_right</mat-icon>
|
||||
|
@@ -41,9 +41,11 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
border-top: 1px solid #ffffff30;
|
||||
border-bottom: 1px solid #ffffff30;
|
||||
padding: .5rem 0;
|
||||
max-height: 310px;
|
||||
overflow-y: auto;
|
||||
border-top: 1px solid rgba(#8795a1, .3);
|
||||
border-bottom: 1px solid rgba(#8795a1, .3);
|
||||
|
||||
.row {
|
||||
padding: .5rem;
|
||||
@@ -84,7 +86,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.title {
|
||||
.user-title {
|
||||
font-weight: 500;
|
||||
font-size: .9rem;
|
||||
line-height: 1rem;
|
||||
@@ -92,7 +94,7 @@
|
||||
|
||||
.email,
|
||||
.loginname {
|
||||
color: #8795a1;
|
||||
color: var(--grey);
|
||||
font-size: .8rem;
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
@@ -21,7 +21,9 @@ export class AccountsCardComponent implements OnInit {
|
||||
this.userService.getMyUserSessions().then(sessions => {
|
||||
this.users = sessions.toObject().userSessionsList;
|
||||
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;
|
||||
}).catch(() => {
|
||||
|
@@ -9,8 +9,9 @@
|
||||
<mat-form-field class="full-width" appearance="outline">
|
||||
<mat-label>{{ 'MEMBER.CREATIONTYPE' | translate }}</mat-label>
|
||||
<mat-select [(ngModel)]="creationType" (selectionChange)="loadRoles()">
|
||||
<mat-option *ngFor="let type of creationTypes" [value]="type">
|
||||
{{ 'MEMBER.CREATIONTYPES.'+type | translate}}
|
||||
<mat-option *ngFor="let type of creationTypes" [value]="type.type"
|
||||
[disabled]="(type.disabled$ | async) == false">
|
||||
{{ 'MEMBER.CREATIONTYPES.'+type.type | translate}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
@@ -3,7 +3,7 @@
|
||||
}
|
||||
|
||||
.desc {
|
||||
color: #8795a1;
|
||||
color: var(--grey);
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
@@ -18,8 +18,4 @@
|
||||
.ok-button {
|
||||
margin-left: .5rem;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: .5rem;
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,9 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
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 { 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 { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
@@ -23,13 +25,18 @@ export class MemberCreateDialogComponent {
|
||||
private grantId: string = '';
|
||||
public preselectedUsers: Array<UserView.AsObject> = [];
|
||||
|
||||
|
||||
public creationType!: CreationType;
|
||||
public creationTypes: CreationType[] = [
|
||||
CreationType.IAM,
|
||||
CreationType.ORG,
|
||||
CreationType.PROJECT_OWNED,
|
||||
CreationType.PROJECT_GRANTED,
|
||||
|
||||
/**
|
||||
* Specifies options for creating members,
|
||||
* without ending $, to enable write event permission even if user is allowed
|
||||
* 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 roles: Array<ProjectRole.AsObject> | string[] = [];
|
||||
@@ -41,6 +48,7 @@ export class MemberCreateDialogComponent {
|
||||
constructor(
|
||||
private mgmtService: ManagementService,
|
||||
private adminService: AdminService,
|
||||
private authService: GrpcAuthService,
|
||||
public dialogRef: MatDialogRef<MemberCreateDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
private toastService: ToastService,
|
||||
|
@@ -1,9 +1,11 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { SearchUserAutocompleteModule } from 'src/app/modules/search-user-autocomplete/search-user-autocomplete.module';
|
||||
@@ -21,10 +23,13 @@ import { MemberCreateDialogComponent } from './member-create-dialog.component';
|
||||
CommonModule,
|
||||
MatDialogModule,
|
||||
MatButtonModule,
|
||||
MatChipsModule,
|
||||
MatInputModule,
|
||||
TranslateModule,
|
||||
MatFormFieldModule,
|
||||
MatSelectModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
SearchUserAutocompleteModule,
|
||||
SearchRolesAutocompleteModule,
|
||||
SearchProjectAutocompleteModule,
|
||||
|
@@ -34,7 +34,7 @@
|
||||
|
||||
.desc {
|
||||
font-size: .9rem;
|
||||
color: #8795a1;
|
||||
color: var(--grey);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -11,7 +11,7 @@
|
||||
|
||||
.card {
|
||||
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);
|
||||
box-sizing: border-box;
|
||||
border-radius: .5rem;
|
||||
|
@@ -21,13 +21,13 @@
|
||||
flex-direction: column;
|
||||
|
||||
.editor {
|
||||
color: #8795a1;
|
||||
color: var(--grey);
|
||||
font-size: 12px;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.seq {
|
||||
color: #8795a1;
|
||||
color: var(--grey);
|
||||
font-size: 12px;
|
||||
align-self: flex-end;
|
||||
}
|
||||
@@ -43,7 +43,7 @@
|
||||
|
||||
&.change-item-back {
|
||||
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 {
|
||||
font-size: 12px;
|
||||
color: #8795a1;
|
||||
color: var(--grey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -103,11 +103,9 @@ export class ChangesComponent implements OnInit {
|
||||
this._loading.next(true);
|
||||
|
||||
return from(col).pipe(
|
||||
take(1),
|
||||
tap((res: Changes) => {
|
||||
let values = res.toObject().changesList;
|
||||
// If prepending, reverse the batch order
|
||||
values = false ? values.reverse() : values;
|
||||
|
||||
const values = res.toObject().changesList;
|
||||
// update source with new values, done loading
|
||||
this._data.next(values);
|
||||
|
||||
@@ -118,12 +116,11 @@ export class ChangesComponent implements OnInit {
|
||||
this._done.next(true);
|
||||
}
|
||||
}),
|
||||
catchError(err => {
|
||||
catchError(_ => {
|
||||
this._loading.next(false);
|
||||
this.bottom = true;
|
||||
return of([]);
|
||||
}),
|
||||
take(1),
|
||||
).subscribe();
|
||||
}
|
||||
}
|
||||
|
@@ -4,9 +4,9 @@ import { NgModule } from '@angular/core';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ScrollableModule } from 'src/app/directives/scrollable/scrollable.module';
|
||||
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe.module';
|
||||
import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe.module';
|
||||
import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe.module';
|
||||
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
|
||||
import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module';
|
||||
import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module';
|
||||
|
||||
import { ChangesComponent } from './changes.component';
|
||||
|
||||
|
@@ -8,13 +8,19 @@
|
||||
<ng-container *ngIf="totalResult < 10; else compact">
|
||||
<ng-container *ngFor="let member of membersSubject | async; index as i">
|
||||
<div @animate (click)="emitShowDetail()" class="avatar-circle"
|
||||
matTooltip="{{ member.email }} | {{member.rolesList?.join(' ')}}"
|
||||
matTooltip="{{ member.displayName }} | {{member.rolesList?.join(' ')}}"
|
||||
[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"
|
||||
[name]="member.displayName ? member.displayName : (member.firstName + ' '+ member.lastName)"
|
||||
[size]="32">
|
||||
</app-avatar>
|
||||
<ng-template #cog>
|
||||
<div class="sa-icon">
|
||||
<i class="las la-user-cog"></i>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
@@ -9,7 +9,7 @@
|
||||
|
||||
.sub-header {
|
||||
font-size: .8rem;
|
||||
color: #8795a1;
|
||||
color: var(--grey);
|
||||
}
|
||||
|
||||
.people {
|
||||
@@ -65,6 +65,17 @@
|
||||
.avatar {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sa-icon {
|
||||
display: block;
|
||||
width: 32px;
|
||||
margin: 0 .5rem;
|
||||
|
||||
i {
|
||||
margin: auto;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.margin-neg {
|
||||
|
@@ -1,14 +1,16 @@
|
||||
<div class="max-width-container detail-container">
|
||||
<div class="detail-left">
|
||||
<a *ngIf="backRouterLink" [routerLink]="backRouterLink" mat-icon-button>
|
||||
<mat-icon class="icon">arrow_back</mat-icon>
|
||||
</a>
|
||||
</div>
|
||||
<div class="detail-right">
|
||||
<div class="head">
|
||||
<h1>{{ title }}</h1>
|
||||
<p class="desc">{{ description }}</p>
|
||||
<ng-content></ng-content>
|
||||
<div class="max-width-container">
|
||||
<div class="detail-container">
|
||||
<div class="detail-left">
|
||||
<a *ngIf="backRouterLink" [routerLink]="backRouterLink" mat-icon-button>
|
||||
<mat-icon class="icon">arrow_back</mat-icon>
|
||||
</a>
|
||||
</div>
|
||||
<div class="detail-right">
|
||||
<div class="head">
|
||||
<h1>{{ title }}</h1>
|
||||
<p class="desc">{{ description }}</p>
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -10,9 +10,9 @@
|
||||
$lighter-color: rgba(mat-color($primary, 300), .5);
|
||||
|
||||
.detail-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
padding-bottom: 3rem;
|
||||
padding-top: 3rem;
|
||||
|
||||
.detail-left {
|
||||
width: 100px;
|
||||
@@ -29,7 +29,6 @@
|
||||
|
||||
.detail-right {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
padding-left: 1rem;
|
||||
|
||||
@media only screen and (max-width: 500px) {
|
||||
@@ -37,20 +36,17 @@
|
||||
}
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
flex-wrap: wrap;
|
||||
|
||||
h1 {
|
||||
font-size: 1.2rem;
|
||||
font-size: 1.5rem;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.desc {
|
||||
width: 100%;
|
||||
display: block;
|
||||
font-size: .9rem;
|
||||
color: #8795a1;
|
||||
color: var(--grey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -11,6 +11,8 @@
|
||||
<h1>{{'IDP.CREATE.TITLE' | translate}}</h1>
|
||||
<p>{{'IDP.CREATE.DESCRIPTION' | translate}}</p>
|
||||
|
||||
<mat-progress-bar *ngIf="loading" color="primary" mode="indeterminate"></mat-progress-bar>
|
||||
|
||||
<form (ngSubmit)="addIdp()">
|
||||
<ng-container [formGroup]="formGroup">
|
||||
<div class="content">
|
||||
@@ -48,22 +50,22 @@
|
||||
</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>
|
||||
<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>
|
||||
|
||||
@@ -71,4 +73,4 @@
|
||||
{{ 'ACTIONS.SAVE' | translate }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
@@ -25,7 +25,6 @@
|
||||
|
||||
.add-line-btn {
|
||||
margin-bottom: 1rem;
|
||||
border-radius: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +37,7 @@
|
||||
flex-basis: 100%;
|
||||
margin: 0 .5rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #8795a1;
|
||||
color: var(--grey);
|
||||
}
|
||||
|
||||
.formfield {
|
||||
@@ -55,7 +54,6 @@
|
||||
margin-top: 3rem;
|
||||
display: block;
|
||||
padding: .5rem 4rem;
|
||||
border-radius: .5rem;
|
||||
|
||||
@media only screen and (max-width: 450px) {
|
||||
margin-top: 1rem;
|
||||
|
@@ -7,18 +7,18 @@ import { ActivatedRoute, Params, Router } from '@angular/router';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
import {
|
||||
OidcIdpConfigCreate as AdminOidcIdpConfigCreate,
|
||||
OIDCMappingField as authMappingFields,
|
||||
OidcIdpConfigCreate as AdminOidcIdpConfigCreate,
|
||||
OIDCMappingField as authMappingFields,
|
||||
} from 'src/app/proto/generated/admin_pb';
|
||||
import { AdminService } from 'src/app/services/admin.service';
|
||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
import { PolicyComponentServiceType } from '../policies/policy-component-types.enum';
|
||||
import {
|
||||
OidcIdpConfigCreate as MgmtOidcIdpConfigCreate,
|
||||
OIDCMappingField as mgmtMappingFields,
|
||||
OidcIdpConfigCreate as MgmtOidcIdpConfigCreate,
|
||||
OIDCMappingField as mgmtMappingFields,
|
||||
} from '../../proto/generated/management_pb';
|
||||
import { PolicyComponentServiceType } from '../policies/policy-component-types.enum';
|
||||
|
||||
@Component({
|
||||
selector: 'app-idp-create',
|
||||
@@ -37,7 +37,7 @@ export class IdpCreateComponent implements OnInit, OnDestroy {
|
||||
public formGroup!: FormGroup;
|
||||
public createSteps: number = 1;
|
||||
public currentCreateStep: number = 1;
|
||||
|
||||
public loading: boolean = false;
|
||||
constructor(
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
@@ -47,7 +47,6 @@ export class IdpCreateComponent implements OnInit, OnDestroy {
|
||||
) {
|
||||
this.formGroup = new FormGroup({
|
||||
name: new FormControl('', [Validators.required]),
|
||||
logoSrc: new FormControl('', []),
|
||||
clientId: new FormControl('', [Validators.required]),
|
||||
clientSecret: new FormControl('', [Validators.required]),
|
||||
issuer: new FormControl('', [Validators.required]),
|
||||
@@ -68,8 +67,8 @@ export class IdpCreateComponent implements OnInit, OnDestroy {
|
||||
case PolicyComponentServiceType.ADMIN:
|
||||
this.service = this.injector.get(AdminService as Type<AdminService>);
|
||||
this.mappingFields = [
|
||||
authMappingFields.OIDCMAPPINGFIELD_PREFERRED_USERNAME,
|
||||
authMappingFields.OIDCMAPPINGFIELD_EMAIL];
|
||||
authMappingFields.OIDCMAPPINGFIELD_PREFERRED_USERNAME,
|
||||
authMappingFields.OIDCMAPPINGFIELD_EMAIL];
|
||||
break;
|
||||
}
|
||||
});
|
||||
@@ -91,25 +90,30 @@ export class IdpCreateComponent implements OnInit, OnDestroy {
|
||||
let req: AdminOidcIdpConfigCreate | MgmtOidcIdpConfigCreate;
|
||||
|
||||
switch (this.serviceType) {
|
||||
case PolicyComponentServiceType.MGMT:
|
||||
req = new MgmtOidcIdpConfigCreate();
|
||||
break;
|
||||
case PolicyComponentServiceType.ADMIN:
|
||||
req = new AdminOidcIdpConfigCreate();
|
||||
break;
|
||||
case PolicyComponentServiceType.MGMT:
|
||||
req = new MgmtOidcIdpConfigCreate();
|
||||
break;
|
||||
case PolicyComponentServiceType.ADMIN:
|
||||
req = new AdminOidcIdpConfigCreate();
|
||||
break;
|
||||
}
|
||||
|
||||
req.setName(this.name?.value);
|
||||
req.setClientId(this.clientId?.value);
|
||||
req.setClientSecret(this.clientSecret?.value);
|
||||
req.setIssuer(this.issuer?.value);
|
||||
req.setLogoSrc(this.logoSrc?.value);
|
||||
req.setScopesList(this.scopesList?.value);
|
||||
req.setIdpDisplayNameMapping(this.idpDisplayNameMapping?.value);
|
||||
req.setUsernameMapping(this.usernameMapping?.value);
|
||||
|
||||
this.loading = true;
|
||||
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 => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
@@ -147,10 +151,6 @@ export class IdpCreateComponent implements OnInit, OnDestroy {
|
||||
return this.formGroup.get('name');
|
||||
}
|
||||
|
||||
public get logoSrc(): AbstractControl | null {
|
||||
return this.formGroup.get('logoSrc');
|
||||
}
|
||||
|
||||
public get clientId(): AbstractControl | null {
|
||||
return this.formGroup.get('clientId');
|
||||
}
|
||||
@@ -163,7 +163,7 @@ export class IdpCreateComponent implements OnInit, OnDestroy {
|
||||
return this.formGroup.get('issuer');
|
||||
}
|
||||
public get scopesList(): AbstractControl | null {
|
||||
return this.formGroup.get('scopesList');
|
||||
return this.formGroup.get('scopesList');
|
||||
}
|
||||
|
||||
public get idpDisplayNameMapping(): AbstractControl | null {
|
||||
@@ -171,7 +171,7 @@ export class IdpCreateComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
public get usernameMapping(): AbstractControl | null {
|
||||
return this.formGroup.get('usernameMapping');
|
||||
return this.formGroup.get('usernameMapping');
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -6,12 +6,13 @@ import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { IdpCreateRoutingModule } from './idp-create-routing.module';
|
||||
import { IdpCreateComponent } from './idp-create.component';
|
||||
import {MatSelectModule} from '@angular/material/select';
|
||||
|
||||
@NgModule({
|
||||
declarations: [IdpCreateComponent],
|
||||
@@ -28,6 +29,7 @@ import {MatSelectModule} from '@angular/material/select';
|
||||
MatChipsModule,
|
||||
MatTooltipModule,
|
||||
TranslateModule,
|
||||
MatProgressBarModule,
|
||||
],
|
||||
})
|
||||
export class IdpCreateModule { }
|
||||
|
@@ -1,19 +1,19 @@
|
||||
<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>
|
||||
<button (click)="deactivateSelectedIdps()" matTooltip="{{'IDP.DEACTIVATE' | translate}}" class="icon-button"
|
||||
mat-icon-button *ngIf="selection.hasValue()" [disabled]="disabled">
|
||||
<mat-icon svgIcon="mdi_account_cancel"></mat-icon>
|
||||
<mat-icon>block</mat-icon>
|
||||
</button>
|
||||
<button (click)="reactivateSelectedIdps()" matTooltip="{{'IDP.ACTIVATE' | translate}}" class="icon-button"
|
||||
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 color="warn" (click)="removeSelectedIdps()" matTooltip="{{'IDP.DELETE' | translate}}"
|
||||
class="icon-button" mat-icon-button *ngIf="selection.hasValue()" [disabled]="disabled">
|
||||
<i class="las la-trash"></i>
|
||||
</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 }}
|
||||
</a>
|
||||
</ng-template>
|
||||
@@ -24,14 +24,14 @@
|
||||
<th mat-header-cell *matHeaderCellDef>
|
||||
<mat-checkbox color="primary" (change)="$event ? masterToggle() : null"
|
||||
[checked]="selection.hasValue() && isAllSelected()"
|
||||
[indeterminate]="selection.hasValue() && !isAllSelected()">
|
||||
[indeterminate]="selection.hasValue() && !isAllSelected()"
|
||||
[disabled]="serviceType==PolicyComponentServiceType.MGMT">
|
||||
</mat-checkbox>
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let idp">
|
||||
<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)">
|
||||
<img *ngIf="idp?.logoSrc?.startsWith('https://'); else genAvatar" [src]="idp.logoSrc"
|
||||
alt="ipp logo {{idp?.name}}" />
|
||||
<ng-template #genAvatar>
|
||||
<div class="avatar">
|
||||
<span>{{idp.name.charAt(0)}}</span>
|
||||
@@ -43,12 +43,12 @@
|
||||
|
||||
<ng-container matColumnDef="name">
|
||||
<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 matColumnDef="config">
|
||||
<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">
|
||||
<span class="key">{{elem.key}}:</span>
|
||||
<span class="value">{{elem.value}}</span>
|
||||
@@ -58,28 +58,47 @@
|
||||
|
||||
<ng-container matColumnDef="state">
|
||||
<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 matColumnDef="creationDate">
|
||||
<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>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="changeDate">
|
||||
<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>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr class="data-row" mat-row *matRowDef="let row; columns: displayedColumns;"
|
||||
[routerLink]="row.id ? [serviceType == PolicyComponentServiceType.ADMIN ? '/iam' : '/org', 'idp', row.id ]: null">
|
||||
</tr>
|
||||
<ng-container matColumnDef="type">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'IDP.TYPE' | translate }} </th>
|
||||
<td [routerLink]="routerLinkForRow(idp)" class="pointer" mat-cell *matCellDef="let idp">
|
||||
{{'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>
|
||||
<mat-paginator #paginator class="paginator" [length]="idpResult?.totalResult || 0" [pageSize]="10"
|
||||
[pageSizeOptions]="[5, 10, 20]" (page)="changePage($event)"></mat-paginator>
|
||||
</div>
|
||||
<mat-paginator #paginator class="paginator" [length]="idpResult?.totalResult || 0" [pageSize]="10"
|
||||
[pageSizeOptions]="[5, 10, 20]" (page)="changePage($event)"></mat-paginator>
|
||||
</app-refresh-table>
|
@@ -1,11 +1,10 @@
|
||||
|
||||
.table-wrapper {
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
|
||||
.table,
|
||||
.paginator {
|
||||
width: 100%;
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: 0 1rem;
|
||||
@@ -19,23 +18,29 @@
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.data-row {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #ffffff05;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
tr {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
border-radius: .5rem;
|
||||
&.disabled * {
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
button {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
button {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
@@ -57,14 +62,22 @@ tr {
|
||||
.flex-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 2px 0;
|
||||
width: 250px;
|
||||
|
||||
.key {
|
||||
margin-right: 1rem;
|
||||
font-size: 14px;
|
||||
margin-right: .5rem;
|
||||
font-size: 12px;
|
||||
flex: 1 0 50%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 14px;
|
||||
font-size: 12px;
|
||||
flex: 1 0 50%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
|
@@ -1,17 +1,19 @@
|
||||
import { SelectionModel } from '@angular/cdk/collections';
|
||||
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatPaginator, PageEvent } from '@angular/material/paginator';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
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 { ManagementService } from 'src/app/services/mgmt.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
import { PolicyComponentServiceType } from '../policies/policy-component-types.enum';
|
||||
import { WarnDialogComponent } from '../warn-dialog/warn-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-idp-table',
|
||||
@@ -31,12 +33,13 @@ export class IdpTableComponent implements OnInit {
|
||||
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
|
||||
public PolicyComponentServiceType: any = PolicyComponentServiceType;
|
||||
public IdpProviderType: any = IdpProviderType;
|
||||
@Input() public displayedColumns: string[] = ['select', 'name', 'config', 'creationDate', 'changeDate', 'state'];
|
||||
|
||||
@Output() public changedSelection: EventEmitter<Array<AdminIdpView.AsObject | MgmtIdpView.AsObject>>
|
||||
= new EventEmitter();
|
||||
|
||||
constructor(public translate: TranslateService, private toast: ToastService) {
|
||||
constructor(public translate: TranslateService, private toast: ToastService, private dialog: MatDialog) {
|
||||
this.selection.changed.subscribe(() => {
|
||||
this.changedSelection.emit(this.selection.selected);
|
||||
});
|
||||
@@ -44,6 +47,13 @@ export class IdpTableComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
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 {
|
||||
@@ -64,47 +74,79 @@ export class IdpTableComponent implements OnInit {
|
||||
}
|
||||
|
||||
public deactivateSelectedIdps(): void {
|
||||
this.selection.clear();
|
||||
Promise.all(this.selection.selected.map(value => {
|
||||
return this.service.DeactivateIdpConfig(value.id);
|
||||
})).then(() => {
|
||||
this.toast.showInfo('USER.TOAST.SELECTEDDEACTIVATED', true);
|
||||
this.getData(10, 0);
|
||||
this.toast.showInfo('IDP.TOAST.SELECTEDDEACTIVATED', true);
|
||||
this.refreshPage();
|
||||
});
|
||||
}
|
||||
|
||||
public reactivateSelectedIdps(): void {
|
||||
this.selection.clear();
|
||||
Promise.all(this.selection.selected.map(value => {
|
||||
return this.service.ReactivateIdpConfig(value.id);
|
||||
})).then(() => {
|
||||
this.toast.showInfo('USER.TOAST.SELECTEDREACTIVATED', true);
|
||||
this.getData(10, 0);
|
||||
this.toast.showInfo('IDP.TOAST.SELECTEDREACTIVATED', true);
|
||||
this.refreshPage();
|
||||
});
|
||||
}
|
||||
|
||||
public removeSelectedIdps(): void {
|
||||
Promise.all(this.selection.selected.map(value => {
|
||||
return this.service.RemoveIdpConfig(value.id);
|
||||
})).then(() => {
|
||||
this.toast.showInfo('USER.TOAST.SELECTEDDEACTIVATED', true);
|
||||
this.getData(10, 0);
|
||||
const dialogRef = this.dialog.open(WarnDialogComponent, {
|
||||
data: {
|
||||
confirmKey: 'ACTIONS.DELETE',
|
||||
cancelKey: 'ACTIONS.CANCEL',
|
||||
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> {
|
||||
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.idpResult = resp.toObject();
|
||||
this.dataSource.data = this.idpResult.resultList;
|
||||
console.log(this.idpResult.resultList);
|
||||
this.loadingSubject.next(false);
|
||||
}).catch(error => {
|
||||
this.toast.showError(error);
|
||||
@@ -123,4 +165,21 @@ export class IdpTableComponent implements OnInit {
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -11,8 +11,9 @@ import { RouterModule } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
|
||||
import { RefreshTableModule } from 'src/app/modules/refresh-table/refresh-table.module';
|
||||
import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe.module';
|
||||
import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-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/timestamp-to-date-pipe.module';
|
||||
import { TruncatePipeModule } from 'src/app/pipes/truncate-pipe/truncate-pipe.module';
|
||||
|
||||
import { IdpTableComponent } from './idp-table.component';
|
||||
|
||||
@@ -34,6 +35,7 @@ import { IdpTableComponent } from './idp-table.component';
|
||||
RouterModule,
|
||||
RefreshTableModule,
|
||||
HasRoleModule,
|
||||
TruncatePipeModule,
|
||||
],
|
||||
exports: [
|
||||
IdpTableComponent,
|
||||
|
@@ -1,91 +1,99 @@
|
||||
<app-detail-layout [backRouterLink]="backroutes" [title]="'IDP.DETAIL.TITLE' | translate"
|
||||
[description]="'IDP.DETAIL.DESCRIPTION' | translate">
|
||||
<div class="container">
|
||||
<form (ngSubmit)="updateIdp()">
|
||||
<ng-container [formGroup]="idpForm">
|
||||
<div class="content">
|
||||
<mat-form-field appearance="outline" class="formfield">
|
||||
<mat-label>{{ 'IDP.ID' | translate }}</mat-label>
|
||||
<input matInput formControlName="id" />
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="formfield">
|
||||
<mat-label>{{ 'IDP.NAME' | translate }}</mat-label>
|
||||
<input matInput formControlName="name" />
|
||||
</mat-form-field>
|
||||
<!--<mat-form-field appearance="outline" class="formfield">
|
||||
<mat-label>{{ 'IDP.LOGOSRC' | translate }}</mat-label>
|
||||
<input matInput formControlName="logoSrc" />
|
||||
</mat-form-field>-->
|
||||
[description]="'IDP.DETAIL.DESCRIPTION' | translate">
|
||||
<div class="container">
|
||||
<form (ngSubmit)="updateIdp()">
|
||||
<ng-container [formGroup]="idpForm">
|
||||
<div class="content">
|
||||
<mat-form-field appearance="outline" class="formfield">
|
||||
<mat-label>{{ 'IDP.ID' | translate }}</mat-label>
|
||||
<input matInput formControlName="id" />
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="formfield">
|
||||
<mat-label>{{ 'IDP.NAME' | translate }}</mat-label>
|
||||
<input matInput formControlName="name" />
|
||||
</mat-form-field>
|
||||
<mat-form-field class="formfield" appearance="outline">
|
||||
<mat-label>{{ 'IDP.STYLE' | translate }}</mat-label>
|
||||
<mat-select formControlName="stylingType">
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<button color="primary" mat-raised-button class="continue-button" [disabled]="idpForm.invalid" type="submit">
|
||||
{{ '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>
|
||||
</app-detail-layout>
|
@@ -9,6 +9,7 @@
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 0 -.5rem;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -16,27 +17,35 @@
|
||||
flex-basis: 100%;
|
||||
margin: 0 .5rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #8795a1;
|
||||
color: var(--grey);
|
||||
}
|
||||
|
||||
.formfield {
|
||||
flex: 1 0 auto;
|
||||
flex: 1 1 auto;
|
||||
margin: 0 .5rem;
|
||||
|
||||
&.fullwidth {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 450px) {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.continue-button {
|
||||
margin-bottom: 4rem;
|
||||
display: block;
|
||||
padding: .5rem 4rem;
|
||||
border-radius: .5rem;
|
||||
.btn-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
@media only screen and (max-width: 450px) {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
.continue-button {
|
||||
margin-bottom: 4rem;
|
||||
display: block;
|
||||
padding: .5rem 4rem;
|
||||
|
||||
@media only screen and (max-width: 450px) {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,20 +1,22 @@
|
||||
import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes';
|
||||
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 { MatChipInputEvent } from '@angular/material/chips';
|
||||
import { ActivatedRoute, Params } from '@angular/router';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { switchMap, take } from 'rxjs/operators';
|
||||
import {
|
||||
OIDCMappingField as authMappingFields,
|
||||
OidcIdpConfigUpdate as AdminOidcIdpConfigUpdate,
|
||||
IdpUpdate as AdminIdpConfigUpdate,
|
||||
IdpStylingType as adminIdpStylingType,
|
||||
IdpUpdate as AdminIdpConfigUpdate,
|
||||
OidcIdpConfigUpdate as AdminOidcIdpConfigUpdate,
|
||||
OIDCMappingField as adminMappingFields,
|
||||
} from 'src/app/proto/generated/admin_pb';
|
||||
import {
|
||||
OIDCMappingField as mgmtMappingFields,
|
||||
OidcIdpConfigUpdate as MgmtOidcIdpConfigUpdate,
|
||||
IdpUpdate as MgmtIdpConfigUpdate,
|
||||
IdpStylingType as mgmtIdpStylingType,
|
||||
IdpUpdate as MgmtIdpConfigUpdate,
|
||||
OidcIdpConfigUpdate as MgmtOidcIdpConfigUpdate,
|
||||
OIDCMappingField as mgmtMappingFields,
|
||||
} from 'src/app/proto/generated/management_pb';
|
||||
import { AdminService } from 'src/app/services/admin.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'],
|
||||
})
|
||||
export class IdpComponent implements OnInit, OnDestroy {
|
||||
public mappingFields: mgmtMappingFields[] | authMappingFields[] = [];
|
||||
public mappingFields: mgmtMappingFields[] | adminMappingFields[] = [];
|
||||
public styleFields: mgmtIdpStylingType[] | adminIdpStylingType[] = [];
|
||||
|
||||
public showIdSecretSection: boolean = false;
|
||||
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
|
||||
private service!: ManagementService | AdminService;
|
||||
@@ -41,7 +45,6 @@ export class IdpComponent implements OnInit, OnDestroy {
|
||||
public oidcConfigForm!: FormGroup;
|
||||
|
||||
constructor(
|
||||
// private router: Router,
|
||||
private toast: ToastService,
|
||||
private injector: Injector,
|
||||
private route: ActivatedRoute,
|
||||
@@ -50,33 +53,38 @@ export class IdpComponent implements OnInit, OnDestroy {
|
||||
this.idpForm = new FormGroup({
|
||||
id: new FormControl({ disabled: true, value: '' }, [Validators.required]),
|
||||
name: new FormControl('', [Validators.required]),
|
||||
logoSrc: new FormControl({ disabled: true, value: '' }, [Validators.required]),
|
||||
stylingType: new FormControl('', [Validators.required]),
|
||||
});
|
||||
|
||||
this.oidcConfigForm = new FormGroup({
|
||||
clientId: new FormControl('', [Validators.required]),
|
||||
clientSecret: new FormControl(''),
|
||||
issuer: new FormControl('', [Validators.required]),
|
||||
scopesList: new FormControl([], []),
|
||||
idpDisplayNameMapping: new FormControl(0),
|
||||
usernameMapping: new FormControl(0),
|
||||
clientId: new FormControl('', [Validators.required]),
|
||||
clientSecret: new FormControl(''),
|
||||
issuer: new FormControl('', [Validators.required]),
|
||||
scopesList: new FormControl([], []),
|
||||
idpDisplayNameMapping: new FormControl(0),
|
||||
usernameMapping: new FormControl(0),
|
||||
});
|
||||
|
||||
this.route.data.pipe(switchMap(data => {
|
||||
console.log(data.serviceType);
|
||||
this.serviceType = data.serviceType;
|
||||
switch (this.serviceType) {
|
||||
case PolicyComponentServiceType.MGMT:
|
||||
this.service = this.injector.get(ManagementService as Type<ManagementService>);
|
||||
this.mappingFields = [
|
||||
mgmtMappingFields.OIDCMAPPINGFIELD_PREFERRED_USERNAME,
|
||||
mgmtMappingFields.OIDCMAPPINGFIELD_EMAIL];
|
||||
mgmtMappingFields.OIDCMAPPINGFIELD_PREFERRED_USERNAME,
|
||||
mgmtMappingFields.OIDCMAPPINGFIELD_EMAIL];
|
||||
this.styleFields = [
|
||||
mgmtIdpStylingType.IDPSTYLINGTYPE_UNSPECIFIED,
|
||||
mgmtIdpStylingType.IDPSTYLINGTYPE_GOOGLE];
|
||||
break;
|
||||
case PolicyComponentServiceType.ADMIN:
|
||||
this.service = this.injector.get(AdminService as Type<AdminService>);
|
||||
this.mappingFields = [
|
||||
authMappingFields.OIDCMAPPINGFIELD_PREFERRED_USERNAME,
|
||||
authMappingFields.OIDCMAPPINGFIELD_EMAIL];
|
||||
adminMappingFields.OIDCMAPPINGFIELD_PREFERRED_USERNAME,
|
||||
adminMappingFields.OIDCMAPPINGFIELD_EMAIL];
|
||||
this.styleFields = [
|
||||
adminIdpStylingType.IDPSTYLINGTYPE_UNSPECIFIED,
|
||||
adminIdpStylingType.IDPSTYLINGTYPE_GOOGLE];
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -85,11 +93,11 @@ export class IdpComponent implements OnInit, OnDestroy {
|
||||
const { id } = params;
|
||||
if (id) {
|
||||
this.service.IdpByID(id).then(idp => {
|
||||
const idpObject = idp.toObject();
|
||||
this.idpForm.patchValue(idpObject);
|
||||
if (idpObject.oidcConfig) {
|
||||
this.oidcConfigForm.patchValue(idpObject.oidcConfig);
|
||||
}
|
||||
const idpObject = idp.toObject();
|
||||
this.idpForm.patchValue(idpObject);
|
||||
if (idpObject.oidcConfig) {
|
||||
this.oidcConfigForm.patchValue(idpObject.oidcConfig);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -121,10 +129,10 @@ export class IdpComponent implements OnInit, OnDestroy {
|
||||
|
||||
req.setId(this.id?.value);
|
||||
req.setName(this.name?.value);
|
||||
req.setLogoSrc(this.logoSrc?.value);
|
||||
req.setStylingType(this.stylingType?.value);
|
||||
|
||||
this.service.UpdateIdp(req).then((idp) => {
|
||||
this.toast.showInfo('IDP.TOAST.SAVED', true);
|
||||
this.toast.showInfo('IDP.TOAST.SAVED', true);
|
||||
// this.router.navigate(['idp', ]);
|
||||
}).catch(error => {
|
||||
this.toast.showError(error);
|
||||
@@ -132,31 +140,31 @@ export class IdpComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
public updateOidcConfig(): void {
|
||||
let req: AdminOidcIdpConfigUpdate | MgmtOidcIdpConfigUpdate;
|
||||
let req: AdminOidcIdpConfigUpdate | MgmtOidcIdpConfigUpdate;
|
||||
|
||||
switch (this.serviceType) {
|
||||
case PolicyComponentServiceType.MGMT:
|
||||
req = new MgmtOidcIdpConfigUpdate();
|
||||
break;
|
||||
case PolicyComponentServiceType.ADMIN:
|
||||
req = new AdminOidcIdpConfigUpdate();
|
||||
break;
|
||||
}
|
||||
switch (this.serviceType) {
|
||||
case PolicyComponentServiceType.MGMT:
|
||||
req = new MgmtOidcIdpConfigUpdate();
|
||||
break;
|
||||
case PolicyComponentServiceType.ADMIN:
|
||||
req = new AdminOidcIdpConfigUpdate();
|
||||
break;
|
||||
}
|
||||
|
||||
req.setIdpId(this.id?.value);
|
||||
req.setClientId(this.clientId?.value);
|
||||
req.setClientSecret(this.clientSecret?.value);
|
||||
req.setIssuer(this.issuer?.value);
|
||||
req.setScopesList(this.scopesList?.value);
|
||||
req.setUsernameMapping(this.usernameMapping?.value);
|
||||
req.setIdpDisplayNameMapping(this.idpDisplayNameMapping?.value);
|
||||
req.setIdpId(this.id?.value);
|
||||
req.setClientId(this.clientId?.value);
|
||||
req.setClientSecret(this.clientSecret?.value);
|
||||
req.setIssuer(this.issuer?.value);
|
||||
req.setScopesList(this.scopesList?.value);
|
||||
req.setUsernameMapping(this.usernameMapping?.value);
|
||||
req.setIdpDisplayNameMapping(this.idpDisplayNameMapping?.value);
|
||||
|
||||
this.service.UpdateOidcIdpConfig(req).then((oidcConfig) => {
|
||||
this.toast.showInfo('IDP.TOAST.SAVED', true);
|
||||
// this.router.navigate(['idp', ]);
|
||||
}).catch(error => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
this.service.UpdateOidcIdpConfig(req).then((oidcConfig) => {
|
||||
this.toast.showInfo('IDP.TOAST.SAVED', true);
|
||||
// this.router.navigate(['idp', ]);
|
||||
}).catch(error => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
@@ -190,12 +198,10 @@ export class IdpComponent implements OnInit, OnDestroy {
|
||||
public get backroutes(): string[] {
|
||||
switch (this.serviceType) {
|
||||
case PolicyComponentServiceType.MGMT:
|
||||
return ['/org', 'policy', 'login'];
|
||||
return ['/org', 'policy', 'login'];
|
||||
case PolicyComponentServiceType.ADMIN:
|
||||
return ['/iam', 'policy', 'login'];
|
||||
break;
|
||||
return ['/iam', 'policy', 'login'];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
public get id(): AbstractControl | null {
|
||||
@@ -206,8 +212,8 @@ export class IdpComponent implements OnInit, OnDestroy {
|
||||
return this.idpForm.get('name');
|
||||
}
|
||||
|
||||
public get logoSrc(): AbstractControl | null {
|
||||
return this.idpForm.get('logoSrc');
|
||||
public get stylingType(): AbstractControl | null {
|
||||
return this.idpForm.get('stylingType');
|
||||
}
|
||||
|
||||
public get clientId(): AbstractControl | null {
|
||||
@@ -227,10 +233,10 @@ export class IdpComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
public get idpDisplayNameMapping(): AbstractControl | null {
|
||||
return this.oidcConfigForm.get('idpDisplayNameMapping');
|
||||
return this.oidcConfigForm.get('idpDisplayNameMapping');
|
||||
}
|
||||
|
||||
public get usernameMapping(): AbstractControl | null {
|
||||
return this.oidcConfigForm.get('usernameMapping');
|
||||
return this.oidcConfigForm.get('usernameMapping');
|
||||
}
|
||||
}
|
||||
|
@@ -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>
|
@@ -1,9 +1,14 @@
|
||||
.icon-button {
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
overflow: auto;
|
||||
overflow-x: auto;
|
||||
|
||||
.table,
|
||||
.paginator {
|
||||
width: 100%;
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: .5rem;
|
||||
@@ -22,20 +27,31 @@
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.data-row {
|
||||
&:hover {
|
||||
background-color: #ffffff05;
|
||||
}
|
||||
}
|
||||
|
||||
.selection {
|
||||
width: 50px;
|
||||
max-width: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.role {
|
||||
display: inline-block;
|
||||
margin: .25rem;
|
||||
tr {
|
||||
button {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
button {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sa-icon {
|
||||
display: block;
|
||||
width: 32px;
|
||||
margin: 0 .5rem;
|
||||
|
||||
i {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
}
|
@@ -4,15 +4,15 @@ import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
import { ProjectGrantMembersComponent } from './project-grant-members.component';
|
||||
import { MembersTableComponent } from './members-table.component';
|
||||
|
||||
describe('ProjectMembersComponent', () => {
|
||||
let component: ProjectGrantMembersComponent;
|
||||
let fixture: ComponentFixture<ProjectGrantMembersComponent>;
|
||||
describe('MembersTableComponent', () => {
|
||||
let component: MembersTableComponent;
|
||||
let fixture: ComponentFixture<MembersTableComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ProjectGrantMembersComponent],
|
||||
declarations: [MembersTableComponent],
|
||||
imports: [
|
||||
NoopAnimationsModule,
|
||||
MatPaginatorModule,
|
||||
@@ -23,7 +23,7 @@ describe('ProjectMembersComponent', () => {
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ProjectGrantMembersComponent);
|
||||
fixture = TestBed.createComponent(MembersTableComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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 { }
|
@@ -2,7 +2,7 @@
|
||||
display: flex;
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
transition: all .2s ease-in-out;
|
||||
transition: all .3s cubic-bezier(.645, .045, .355, 1);
|
||||
|
||||
.main-content {
|
||||
display: relative;
|
||||
|
@@ -1,6 +1,7 @@
|
||||
|
||||
.validation-col {
|
||||
display: flex wrap;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 1rem 0;
|
||||
width: 100%;
|
||||
|
||||
@@ -16,7 +17,7 @@
|
||||
|
||||
span {
|
||||
font-size: 14px;
|
||||
color: #8795a1;
|
||||
color: var(--grey);
|
||||
}
|
||||
|
||||
.sp-wrapper {
|
||||
|
@@ -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 { }
|
@@ -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>
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -1,20 +1,20 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { IamPolicyGridComponent } from './iam-policy-grid.component';
|
||||
import { LabelPolicyComponent } from './label-policy.component';
|
||||
|
||||
describe('IamPolicyGridComponent', () => {
|
||||
let component: IamPolicyGridComponent;
|
||||
let fixture: ComponentFixture<IamPolicyGridComponent>;
|
||||
describe('LabelPolicyComponent', () => {
|
||||
let component: LabelPolicyComponent;
|
||||
let fixture: ComponentFixture<LabelPolicyComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [IamPolicyGridComponent],
|
||||
declarations: [LabelPolicyComponent],
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(IamPolicyGridComponent);
|
||||
fixture = TestBed.createComponent(LabelPolicyComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
@@ -11,13 +11,13 @@ 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 { PasswordIamPolicyRoutingModule } from './password-iam-policy-routing.module';
|
||||
import { PasswordIamPolicyComponent } from './password-iam-policy.component';
|
||||
import { LabelPolicyRoutingModule } from './label-policy-routing.module';
|
||||
import { LabelPolicyComponent } from './label-policy.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [PasswordIamPolicyComponent],
|
||||
declarations: [LabelPolicyComponent],
|
||||
imports: [
|
||||
PasswordIamPolicyRoutingModule,
|
||||
LabelPolicyRoutingModule,
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
MatInputModule,
|
||||
@@ -31,4 +31,4 @@ import { PasswordIamPolicyComponent } from './password-iam-policy.component';
|
||||
DetailLayoutModule,
|
||||
],
|
||||
})
|
||||
export class PasswordIamPolicyModule { }
|
||||
export class LabelPolicyModule { }
|
@@ -6,7 +6,7 @@
|
||||
<div mat-dialog-content>
|
||||
<mat-form-field *ngIf="serviceType == PolicyComponentServiceType.MGMT" class="full-width" appearance="outline">
|
||||
<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">
|
||||
{{ 'IDP.TYPES.'+type | translate}}
|
||||
</mat-option>
|
||||
|
@@ -3,7 +3,7 @@
|
||||
}
|
||||
|
||||
.desc {
|
||||
color: #8795a1;
|
||||
color: var(--grey);
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
@@ -18,8 +18,4 @@
|
||||
.ok-button {
|
||||
margin-left: .5rem;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: .5rem;
|
||||
}
|
||||
}
|
||||
|
@@ -61,7 +61,9 @@ export class AddIdpDialogComponent {
|
||||
query.setKey(IdpSearchKey.IDPSEARCHKEY_PROVIDER_TYPE);
|
||||
query.setMethod(SearchMethod.SEARCHMETHOD_EQUALS);
|
||||
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;
|
||||
});
|
||||
} else if (this.serviceType === PolicyComponentServiceType.ADMIN) {
|
||||
|
@@ -1,60 +1,76 @@
|
||||
<app-detail-layout [backRouterLink]="backroutes" [title]="'ORG.POLICY.LOGIN_POLICY.TITLECREATE' | translate"
|
||||
[description]="'ORG.POLICY.LOGIN_POLICY.DESCRIPTIONCREATE' | translate">
|
||||
<ng-container *ngIf="(['policy.delete'] | hasRole | async) && serviceType == PolicyComponentServiceType.MGMT">
|
||||
<button matTooltip="{{'ORG.POLICY.DELETE' | translate}}" color="warn" (click)="deletePolicy()"
|
||||
mat-stroked-button>
|
||||
{{'ORG.POLICY.DELETE' | translate}}
|
||||
</button>
|
||||
<app-detail-layout [backRouterLink]="[ serviceType === PolicyComponentServiceType.ADMIN ? '/iam' : '/org']"
|
||||
[title]="'POLICY.LOGIN_POLICY.TITLE' | translate"
|
||||
[description]="(serviceType==PolicyComponentServiceType.MGMT ? 'POLICY.LOGIN_POLICY.DESCRIPTIONCREATEMGMT' : PolicyComponentServiceType.ADMIN ? 'POLICY.LOGIN_POLICY.DESCRIPTIONCREATEADMIN' : '') | translate">
|
||||
<p class="default" *ngIf="isDefault"> {{'POLICY.DEFAULTLABEL' | translate}}</p>
|
||||
|
||||
<div class="spinner-wr">
|
||||
<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>
|
||||
|
||||
<div class="content" *ngIf="loginData">
|
||||
<div class="row">
|
||||
<span class="left-desc">{{'ORG.POLICY.DATA.ALLOWUSERNAMEPASSWORD' | translate}}</span>
|
||||
<span class="fill-space"></span>
|
||||
<mat-slide-toggle color="primary" name="hasNumber" ngDefaultControl
|
||||
<mat-slide-toggle class="toggle" color="primary" [disabled]="disabled" ngDefaultControl
|
||||
[(ngModel)]="loginData.allowUsernamePassword">
|
||||
{{'POLICY.DATA.ALLOWUSERNAMEPASSWORD' | translate}}
|
||||
</mat-slide-toggle>
|
||||
<p>{{'POLICY.DATA.ALLOWUSERNAMEPASSWORD_DESC' | translate}}</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="left-desc">{{'ORG.POLICY.DATA.ALLOWREGISTER' | translate}}</span>
|
||||
<span class="fill-space"></span>
|
||||
<mat-slide-toggle color="primary" name="hasNumber" ngDefaultControl [(ngModel)]="loginData.allowRegister">
|
||||
<mat-slide-toggle class="toggle" color="primary" [disabled]="disabled" ngDefaultControl
|
||||
[(ngModel)]="loginData.allowRegister">
|
||||
{{'POLICY.DATA.ALLOWREGISTER' | translate}}
|
||||
</mat-slide-toggle>
|
||||
<p> {{'POLICY.DATA.ALLOWREGISTER_DESC' | translate}} </p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="left-desc">{{'ORG.POLICY.DATA.ALLOWEXTERNALIDP' | translate}}</span>
|
||||
<span class="fill-space"></span>
|
||||
<mat-slide-toggle color="primary" name="hasNumber" ngDefaultControl
|
||||
<mat-slide-toggle class="toggle" color="primary" [disabled]="disabled" ngDefaultControl
|
||||
[(ngModel)]="loginData.allowExternalIdp">
|
||||
{{'POLICY.DATA.ALLOWEXTERNALIDP' | translate}}
|
||||
</mat-slide-toggle>
|
||||
<p> {{'POLICY.DATA.ALLOWEXTERNALIDP_DESC' | translate}} </p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="subheader">{{'LOGINPOLICY.IDPS' | translate}}</p>
|
||||
<h3 class="subheader">{{'LOGINPOLICY.IDPS' | translate}}</h3>
|
||||
|
||||
<div class="idps">
|
||||
<div class="idp" *ngFor="let idp of idps">
|
||||
<mat-icon (click)="removeIdp(idp)" class="rm">remove_circle</mat-icon>
|
||||
<span>{{idp.name}}</span>
|
||||
<div class="idp" [ngClass]="{'disabled': disabled}" *ngFor="let idp of idps">
|
||||
<button [disabled]="disabled" mat-icon-button (click)="removeIdp(idp)" class="rm">
|
||||
<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.ID' | translate }}: {{idp.idpConfigId}}</span>
|
||||
</div>
|
||||
<div class="new-idp" (click)="openDialog()">
|
||||
<div *ngIf="!disabled" class="new-idp" (click)="openDialog()">
|
||||
<mat-icon>add</mat-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-container">
|
||||
<button (click)="savePolicy()" color="primary" type="submit"
|
||||
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
|
||||
</div>
|
||||
|
||||
<button [disabled]="disabled" class="save-button" (click)="savePolicy()" color="primary" type="submit"
|
||||
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
|
||||
|
||||
<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"
|
||||
[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-card>
|
||||
</ng-template>
|
||||
</app-detail-layout>
|
||||
</app-detail-layout>
|
@@ -1,46 +1,36 @@
|
||||
.default {
|
||||
color: #5282c1;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: .5rem;
|
||||
.spinner-wr {
|
||||
margin: .5rem 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: .5rem 0;
|
||||
|
||||
.left-desc {
|
||||
color: #8795a1;
|
||||
font-size: .9rem;
|
||||
.toggle {
|
||||
margin: .3rem 0;
|
||||
}
|
||||
|
||||
.fill-space {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.length-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
p {
|
||||
margin-top: .5rem;
|
||||
font-size: 14px;
|
||||
color: var(--grey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
.save-button {
|
||||
margin-top: 3rem;
|
||||
display: block;
|
||||
padding: .5rem 4rem;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 3rem;
|
||||
display: block;
|
||||
padding: .5rem 4rem;
|
||||
border-radius: .5rem;
|
||||
}
|
||||
.idp-table-card {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.subheader {
|
||||
@@ -58,10 +48,16 @@ button {
|
||||
justify-content: center;
|
||||
margin: .5rem;
|
||||
padding: 10px;
|
||||
border: 1px solid #8795a1;
|
||||
border: 1px solid var(--grey);
|
||||
border-radius: .5rem;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
min-height: 70px;
|
||||
min-width: 150px;
|
||||
|
||||
.name {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
span {
|
||||
padding: 2px;
|
||||
@@ -69,6 +65,7 @@ button {
|
||||
|
||||
.meta {
|
||||
font-size: 12px;
|
||||
color: var(--grey);
|
||||
}
|
||||
|
||||
.rm {
|
||||
@@ -77,10 +74,16 @@ button {
|
||||
left: 0;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
cursor: pointer;
|
||||
|
||||
&[disabled] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #ffffff10;
|
||||
&:not(.disabled) {
|
||||
&:hover {
|
||||
background-color: #ffffff10;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { Component, Injector, OnDestroy, Type } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Empty } from 'google-protobuf/google/protobuf/empty_pb';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
import {
|
||||
@@ -11,11 +10,11 @@ import {
|
||||
IdpView as AdminIdpView,
|
||||
} from 'src/app/proto/generated/admin_pb';
|
||||
import {
|
||||
IdpProviderType,
|
||||
IdpProviderView as MgmtIdpProviderView,
|
||||
IdpView as MgmtIdpView,
|
||||
LoginPolicy,
|
||||
LoginPolicyView, OrgDomainView,
|
||||
IdpProviderType,
|
||||
IdpProviderView as MgmtIdpProviderView,
|
||||
IdpView as MgmtIdpView,
|
||||
LoginPolicy,
|
||||
LoginPolicyView,
|
||||
} from 'src/app/proto/generated/management_pb';
|
||||
import { AdminService } from 'src/app/services/admin.service';
|
||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||
@@ -34,18 +33,19 @@ export class LoginPolicyComponent implements OnDestroy {
|
||||
|
||||
private sub: Subscription = new Subscription();
|
||||
public service!: ManagementService | AdminService;
|
||||
PolicyComponentServiceType: any = PolicyComponentServiceType;
|
||||
public PolicyComponentServiceType: any = PolicyComponentServiceType;
|
||||
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
|
||||
public idps: MgmtIdpProviderView.AsObject[] | AdminIdpProviderView.AsObject[] = [];
|
||||
|
||||
public loading: boolean = false;
|
||||
public disabled: boolean = true;
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private toast: ToastService,
|
||||
private dialog: MatDialog,
|
||||
private injector: Injector,
|
||||
) {
|
||||
this.sub = this.route.data.pipe(switchMap(data => {
|
||||
console.log(data.serviceType);
|
||||
this.serviceType = data.serviceType;
|
||||
switch (this.serviceType) {
|
||||
case PolicyComponentServiceType.MGMT:
|
||||
@@ -58,15 +58,20 @@ export class LoginPolicyComponent implements OnDestroy {
|
||||
|
||||
return this.route.params;
|
||||
})).subscribe(() => {
|
||||
this.getData().then(data => {
|
||||
if (data) {
|
||||
this.loginData = data.toObject();
|
||||
}
|
||||
});
|
||||
this.getIdps().then(idps => {
|
||||
console.log(idps);
|
||||
this.idps = idps;
|
||||
});
|
||||
this.fetchData();
|
||||
});
|
||||
}
|
||||
|
||||
private fetchData(): void {
|
||||
this.getData().then(data => {
|
||||
if (data) {
|
||||
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.setAllowRegister(this.loginData.allowRegister);
|
||||
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:
|
||||
const adminreq = new DefaultLoginPolicy();
|
||||
adminreq.setAllowExternalIdp(this.loginData.allowExternalIdp);
|
||||
@@ -119,25 +128,27 @@ export class LoginPolicyComponent implements OnDestroy {
|
||||
|
||||
public savePolicy(): void {
|
||||
this.updateData().then(() => {
|
||||
switch (this.serviceType) {
|
||||
case PolicyComponentServiceType.MGMT:
|
||||
this.router.navigate(['org']);
|
||||
break;
|
||||
case PolicyComponentServiceType.ADMIN:
|
||||
this.router.navigate(['iam']);
|
||||
break;
|
||||
}
|
||||
this.toast.showInfo('POLICY.LOGIN_POLICY.SAVED', true);
|
||||
this.loading = true;
|
||||
setTimeout(() => {
|
||||
this.fetchData();
|
||||
}, 2000);
|
||||
}).catch(error => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
public deletePolicy(): Promise<Empty> {
|
||||
switch (this.serviceType) {
|
||||
case PolicyComponentServiceType.MGMT:
|
||||
return (this.service as ManagementService).RemoveLoginPolicy();
|
||||
case PolicyComponentServiceType.ADMIN:
|
||||
return (this.service as AdminService).GetDefaultLoginPolicy();
|
||||
public removePolicy(): void {
|
||||
if (this.serviceType === PolicyComponentServiceType.MGMT) {
|
||||
(this.service as ManagementService).RemoveLoginPolicy().then(() => {
|
||||
this.toast.showInfo('POLICY.TOAST.RESETSUCCESS', true);
|
||||
this.loading = true;
|
||||
setTimeout(() => {
|
||||
this.fetchData();
|
||||
}, 2000);
|
||||
}).catch(error => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +162,14 @@ export class LoginPolicyComponent implements OnDestroy {
|
||||
|
||||
dialogRef.afterClosed().subscribe(resp => {
|
||||
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 {
|
||||
switch (this.serviceType) {
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
public get backroutes(): string[] {
|
||||
switch (this.serviceType) {
|
||||
case PolicyComponentServiceType.MGMT:
|
||||
return ['/org'];
|
||||
case PolicyComponentServiceType.ADMIN:
|
||||
return ['/iam'];
|
||||
break;
|
||||
public get isDefault(): boolean {
|
||||
if (this.loginData && this.serviceType === PolicyComponentServiceType.MGMT) {
|
||||
return (this.loginData as LoginPolicyView.AsObject).pb_default;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -2,21 +2,22 @@ import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { CardModule } from 'src/app/modules/card/card.module';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { 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 { CardModule } from 'src/app/modules/card/card.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 { LoginPolicyRoutingModule } from './login-policy-routing.module';
|
||||
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({
|
||||
declarations: [LoginPolicyComponent],
|
||||
@@ -37,6 +38,7 @@ import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
|
||||
DetailLayoutModule,
|
||||
AddIdpDialogModule,
|
||||
IdpTableModule,
|
||||
MatProgressSpinnerModule,
|
||||
],
|
||||
})
|
||||
export class LoginPolicyModule { }
|
||||
|
@@ -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 { }
|
@@ -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>
|
@@ -1,6 +1,6 @@
|
||||
|
||||
button {
|
||||
border-radius: .5rem;
|
||||
.default {
|
||||
color: #5282c1;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
@@ -12,10 +12,9 @@ button {
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: .5rem 0;
|
||||
padding: .3rem 0;
|
||||
|
||||
.left-desc {
|
||||
color: #8795a1;
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
@@ -39,6 +38,5 @@ button {
|
||||
margin-top: 3rem;
|
||||
display: block;
|
||||
padding: .5rem 4rem;
|
||||
border-radius: .5rem;
|
||||
}
|
||||
}
|
@@ -1,20 +1,20 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PasswordIamPolicyComponent } from './password-iam-policy.component';
|
||||
import { OrgIamPolicyComponent } from './org-iam-policy.component';
|
||||
|
||||
describe('PasswordIamPolicyComponent', () => {
|
||||
let component: PasswordIamPolicyComponent;
|
||||
let fixture: ComponentFixture<PasswordIamPolicyComponent>;
|
||||
describe('OrgIamPolicyComponent', () => {
|
||||
let component: OrgIamPolicyComponent;
|
||||
let fixture: ComponentFixture<OrgIamPolicyComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [PasswordIamPolicyComponent],
|
||||
declarations: [OrgIamPolicyComponent],
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PasswordIamPolicyComponent);
|
||||
fixture = TestBed.createComponent(OrgIamPolicyComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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 { }
|
@@ -1,7 +1,6 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { PolicyComponentAction } from '../policy-component-action.enum';
|
||||
import { PasswordAgePolicyComponent } from './password-age-policy.component';
|
||||
|
||||
const routes: Routes = [
|
||||
@@ -10,15 +9,6 @@ const routes: Routes = [
|
||||
component: PasswordAgePolicyComponent,
|
||||
data: {
|
||||
animation: 'DetailPage',
|
||||
action: PolicyComponentAction.MODIFY,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'create',
|
||||
component: PasswordAgePolicyComponent,
|
||||
data: {
|
||||
animation: 'DetailPage',
|
||||
action: PolicyComponentAction.CREATE,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
@@ -1,19 +1,15 @@
|
||||
<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}}
|
||||
<app-detail-layout [backRouterLink]="[ serviceType === PolicyComponentServiceType.ADMIN ? '/iam' : '/org']"
|
||||
[title]="'POLICY.PWD_AGE.TITLE' | translate" [description]="'POLICY.PWD_AGE.DESCRIPTION' | translate">
|
||||
<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>
|
||||
</ng-template>
|
||||
|
||||
<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">
|
||||
<span class="left-desc">{{'ORG.POLICY.DATA.EXPIREWARNDAYS' | translate}}</span>
|
||||
<span class="left-desc">{{'POLICY.DATA.EXPIREWARNDAYS' | translate}}</span>
|
||||
<span class="fill-space"></span>
|
||||
<div class="length-wrapper">
|
||||
<button mat-icon-button (click)="incrementExpireWarnDays()">
|
||||
@@ -27,7 +23,7 @@
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<div class="length-wrapper">
|
||||
<button mat-icon-button (click)="incrementMaxAgeDays()">
|
||||
|
@@ -1,8 +1,3 @@
|
||||
|
||||
button {
|
||||
border-radius: .5rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 1rem;
|
||||
display: flex;
|
||||
@@ -12,10 +7,10 @@ button {
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: .5rem 0;
|
||||
padding: .3rem 0;
|
||||
|
||||
.left-desc {
|
||||
color: #8795a1;
|
||||
color: var(--grey);
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
@@ -26,6 +21,7 @@ button {
|
||||
.length-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: -.6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,6 +35,5 @@ button {
|
||||
margin-top: 3rem;
|
||||
display: block;
|
||||
padding: .5rem 4rem;
|
||||
border-radius: .5rem;
|
||||
}
|
||||
}
|
||||
|
@@ -1,17 +1,14 @@
|
||||
import { Component, OnDestroy } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Component, Injector, OnDestroy, Type } from '@angular/core';
|
||||
import { ActivatedRoute } 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 { DefaultPasswordAgePolicyView } from 'src/app/proto/generated/admin_pb';
|
||||
import { PasswordAgePolicyView } from 'src/app/proto/generated/management_pb';
|
||||
import { AdminService } from 'src/app/services/admin.service';
|
||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
import { PolicyComponentAction } from '../policy-component-action.enum';
|
||||
import { PolicyComponentServiceType } from '../policy-component-types.enum';
|
||||
|
||||
|
||||
@Component({
|
||||
@@ -20,37 +17,37 @@ import { PolicyComponentAction } from '../policy-component-action.enum';
|
||||
styleUrls: ['./password-age-policy.component.scss'],
|
||||
})
|
||||
export class PasswordAgePolicyComponent implements OnDestroy {
|
||||
public title: string = '';
|
||||
public desc: string = '';
|
||||
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
|
||||
public service!: AdminService | ManagementService;
|
||||
|
||||
componentAction: PolicyComponentAction = PolicyComponentAction.CREATE;
|
||||
|
||||
public PolicyComponentAction: any = PolicyComponentAction;
|
||||
|
||||
public ageData!: PasswordAgePolicy.AsObject;
|
||||
public ageData!: PasswordAgePolicyView.AsObject | DefaultPasswordAgePolicyView.AsObject;
|
||||
|
||||
private sub: Subscription = new Subscription();
|
||||
|
||||
public PolicyComponentServiceType: any = PolicyComponentServiceType;
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private mgmtService: ManagementService,
|
||||
private router: Router,
|
||||
private toast: ToastService,
|
||||
private injector: Injector,
|
||||
) {
|
||||
this.sub = this.route.data.pipe(switchMap(data => {
|
||||
this.componentAction = data.action;
|
||||
return this.route.params;
|
||||
})).subscribe(params => {
|
||||
this.title = 'ORG.POLICY.PWD_AGE.TITLECREATE';
|
||||
this.desc = 'ORG.POLICY.PWD_AGE.DESCRIPTIONCREATE';
|
||||
|
||||
if (this.componentAction === PolicyComponentAction.MODIFY) {
|
||||
this.getData(params).then(data => {
|
||||
if (data) {
|
||||
this.ageData = data.toObject() as PasswordAgePolicy.AsObject;
|
||||
}
|
||||
});
|
||||
this.serviceType = data.serviceType;
|
||||
switch (this.serviceType) {
|
||||
case PolicyComponentServiceType.MGMT:
|
||||
this.service = this.injector.get(ManagementService as Type<ManagementService>);
|
||||
break;
|
||||
case PolicyComponentServiceType.ADMIN:
|
||||
this.service = this.injector.get(AdminService as Type<AdminService>);
|
||||
break;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
private async getData(params: any):
|
||||
Promise<PasswordLockoutPolicy | PasswordAgePolicy | PasswordComplexityPolicy | OrgIamPolicy | undefined> {
|
||||
this.title = 'ORG.POLICY.PWD_AGE.TITLE';
|
||||
this.desc = 'ORG.POLICY.PWD_AGE.DESCRIPTION';
|
||||
return this.mgmtService.GetPasswordAgePolicy();
|
||||
private async getData():
|
||||
Promise<PasswordAgePolicyView | DefaultPasswordAgePolicyView> {
|
||||
|
||||
switch (this.serviceType) {
|
||||
case PolicyComponentServiceType.MGMT:
|
||||
return (this.service as ManagementService).GetPasswordAgePolicy();
|
||||
case PolicyComponentServiceType.ADMIN:
|
||||
return (this.service as AdminService).GetDefaultPasswordAgePolicy();
|
||||
}
|
||||
}
|
||||
|
||||
public deletePolicy(): void {
|
||||
this.mgmtService.DeletePasswordAgePolicy(this.ageData.id).then(() => {
|
||||
this.toast.showInfo('Successfully deleted');
|
||||
}).catch(error => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
public removePolicy(): void {
|
||||
if (this.serviceType === PolicyComponentServiceType.MGMT) {
|
||||
(this.service as ManagementService).RemovePasswordAgePolicy().then(() => {
|
||||
this.toast.showInfo('POLICY.TOAST.RESETSUCCESS', true);
|
||||
setTimeout(() => {
|
||||
this.getData();
|
||||
}, 1000);
|
||||
}).catch(error => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public incrementExpireWarnDays(): void {
|
||||
@@ -98,29 +104,46 @@ export class PasswordAgePolicyComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
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(
|
||||
this.ageData.description,
|
||||
this.ageData.maxAgeDays,
|
||||
this.ageData.expireWarnDays,
|
||||
).then(() => {
|
||||
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);
|
||||
});
|
||||
public get isDefault(): boolean {
|
||||
if (this.ageData && this.serviceType === PolicyComponentServiceType.MGMT) {
|
||||
return (this.ageData as PasswordAgePolicyView.AsObject).pb_default;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { PolicyComponentAction } from '../policy-component-action.enum';
|
||||
import { PasswordComplexityPolicyComponent } from './password-complexity-policy.component';
|
||||
|
||||
const routes: Routes = [
|
||||
@@ -10,15 +9,6 @@ const routes: Routes = [
|
||||
component: PasswordComplexityPolicyComponent,
|
||||
data: {
|
||||
animation: 'DetailPage',
|
||||
action: PolicyComponentAction.MODIFY,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'create',
|
||||
component: PasswordComplexityPolicyComponent,
|
||||
data: {
|
||||
animation: 'DetailPage',
|
||||
action: PolicyComponentAction.CREATE,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
@@ -1,19 +1,22 @@
|
||||
<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}}
|
||||
<app-detail-layout [backRouterLink]="[ serviceType === PolicyComponentServiceType.ADMIN ? '/iam' : '/org']"
|
||||
[title]="'POLICY.PWD_COMPLEXITY.TITLE' | translate" [description]="'POLICY.PWD_COMPLEXITY.DESCRIPTION' | translate">
|
||||
|
||||
<p class="default" *ngIf="isDefault"> {{'POLICY.DEFAULTLABEL' | translate}}</p>
|
||||
|
||||
<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>
|
||||
</ng-template>
|
||||
|
||||
<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">
|
||||
<span class="left-desc">{{'ORG.POLICY.DATA.MINLENGTH' | translate}}</span>
|
||||
<span class="left-desc">{{'POLICY.DATA.MINLENGTH' | translate}}</span>
|
||||
<span class="fill-space"></span>
|
||||
<div class="length-wrapper">
|
||||
<button mat-icon-button (click)="decrementLength()">
|
||||
@@ -26,26 +29,26 @@
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<mat-slide-toggle color="primary" name="hasNumber" ngDefaultControl [(ngModel)]="complexityData.hasNumber">
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
<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>
|
||||
<mat-slide-toggle color="primary" name="hasSymbol" ngDefaultControl [(ngModel)]="complexityData.hasSymbol">
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
<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>
|
||||
<mat-slide-toggle color="primary" name="hasLowercase" ngDefaultControl
|
||||
[(ngModel)]="complexityData.hasLowercase">
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
<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>
|
||||
<mat-slide-toggle color="primary" name="hasUppercase" ngDefaultControl
|
||||
[(ngModel)]="complexityData.hasUppercase">
|
||||
@@ -54,7 +57,7 @@
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</app-detail-layout>
|
@@ -1,6 +1,10 @@
|
||||
.default {
|
||||
color: #5282c1;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: .5rem;
|
||||
.spinner-wr {
|
||||
margin: .5rem 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
@@ -12,10 +16,9 @@ button {
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: .5rem 0;
|
||||
padding: .3rem 0;
|
||||
|
||||
.left-desc {
|
||||
color: #8795a1;
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
@@ -26,6 +29,7 @@ button {
|
||||
.length-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: -.6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,6 +43,5 @@ button {
|
||||
margin-top: 3rem;
|
||||
display: block;
|
||||
padding: .5rem 4rem;
|
||||
border-radius: .5rem;
|
||||
}
|
||||
}
|
||||
|
@@ -1,17 +1,14 @@
|
||||
import { Component, OnDestroy } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Component, Injector, OnDestroy, Type } from '@angular/core';
|
||||
import { ActivatedRoute } 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 { DefaultPasswordComplexityPolicy } from 'src/app/proto/generated/admin_pb';
|
||||
import { PasswordComplexityPolicyView } from 'src/app/proto/generated/management_pb';
|
||||
import { AdminService } from 'src/app/services/admin.service';
|
||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
import { PolicyComponentAction } from '../policy-component-action.enum';
|
||||
import { PolicyComponentServiceType } from '../policy-component-types.enum';
|
||||
|
||||
@Component({
|
||||
selector: 'app-password-policy',
|
||||
@@ -19,36 +16,45 @@ import { PolicyComponentAction } from '../policy-component-action.enum';
|
||||
styleUrls: ['./password-complexity-policy.component.scss'],
|
||||
})
|
||||
export class PasswordComplexityPolicyComponent implements OnDestroy {
|
||||
public title: string = '';
|
||||
public desc: string = '';
|
||||
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
|
||||
public service!: ManagementService | AdminService;
|
||||
|
||||
componentAction: PolicyComponentAction = PolicyComponentAction.CREATE;
|
||||
|
||||
public PolicyComponentAction: any = PolicyComponentAction;
|
||||
|
||||
public complexityData!: PasswordComplexityPolicy.AsObject;
|
||||
public complexityData!: PasswordComplexityPolicyView.AsObject | DefaultPasswordComplexityPolicy.AsObject;
|
||||
|
||||
private sub: Subscription = new Subscription();
|
||||
public PolicyComponentServiceType: any = PolicyComponentServiceType;
|
||||
|
||||
public loading: boolean = false;
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private mgmtService: ManagementService,
|
||||
private router: Router,
|
||||
private toast: ToastService,
|
||||
private injector: Injector,
|
||||
) {
|
||||
this.sub = this.route.data.pipe(switchMap(data => {
|
||||
this.componentAction = data.action;
|
||||
return this.route.params;
|
||||
})).subscribe(params => {
|
||||
this.title = 'ORG.POLICY.PWD_COMPLEXITY.TITLECREATE';
|
||||
this.desc = 'ORG.POLICY.PWD_COMPLEXITY.DESCRIPTIONCREATE';
|
||||
this.serviceType = data.serviceType;
|
||||
|
||||
if (this.componentAction === PolicyComponentAction.MODIFY) {
|
||||
this.getData(params).then(data => {
|
||||
if (data) {
|
||||
this.complexityData = data.toObject() as PasswordComplexityPolicy.AsObject;
|
||||
}
|
||||
});
|
||||
switch (this.serviceType) {
|
||||
case PolicyComponentServiceType.MGMT:
|
||||
this.service = this.injector.get(ManagementService as Type<ManagementService>);
|
||||
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();
|
||||
}
|
||||
|
||||
private async getData(params: any):
|
||||
Promise<PasswordLockoutPolicy | PasswordAgePolicy | PasswordComplexityPolicy | OrgIamPolicy | undefined> {
|
||||
this.title = 'ORG.POLICY.PWD_COMPLEXITY.TITLE';
|
||||
this.desc = 'ORG.POLICY.PWD_COMPLEXITY.DESCRIPTION';
|
||||
return this.mgmtService.GetPasswordComplexityPolicy();
|
||||
private async getData():
|
||||
Promise<PasswordComplexityPolicyView | DefaultPasswordComplexityPolicy> {
|
||||
switch (this.serviceType) {
|
||||
case PolicyComponentServiceType.MGMT:
|
||||
return (this.service as ManagementService).GetPasswordComplexityPolicy();
|
||||
case PolicyComponentServiceType.ADMIN:
|
||||
return (this.service as AdminService).GetDefaultPasswordComplexityPolicy();
|
||||
}
|
||||
}
|
||||
|
||||
public deletePolicy(): void {
|
||||
this.mgmtService.DeletePasswordComplexityPolicy(this.complexityData.id).then(() => {
|
||||
this.toast.showInfo('Successfully deleted');
|
||||
}).catch(error => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
public removePolicy(): void {
|
||||
if (this.service instanceof ManagementService) {
|
||||
this.service.removePasswordComplexityPolicy().then(() => {
|
||||
this.toast.showInfo('POLICY.TOAST.RESETSUCCESS', true);
|
||||
setTimeout(() => {
|
||||
this.fetchData();
|
||||
}, 1000);
|
||||
}).catch(error => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public incrementLength(): void {
|
||||
@@ -85,35 +99,56 @@ export class PasswordComplexityPolicyComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
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.description,
|
||||
this.complexityData.hasLowercase,
|
||||
this.complexityData.hasUppercase,
|
||||
this.complexityData.hasNumber,
|
||||
this.complexityData.hasSymbol,
|
||||
this.complexityData.minLength,
|
||||
).then(() => {
|
||||
this.router.navigate(['org']);
|
||||
}).catch(error => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
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);
|
||||
});
|
||||
} 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) {
|
||||
|
||||
this.mgmtService.UpdatePasswordComplexityPolicy(
|
||||
this.complexityData.description,
|
||||
this.complexityData.hasLowercase,
|
||||
this.complexityData.hasUppercase,
|
||||
this.complexityData.hasNumber,
|
||||
this.complexityData.hasSymbol,
|
||||
this.complexityData.minLength,
|
||||
).then(() => {
|
||||
this.router.navigate(['org']);
|
||||
}).catch(error => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
public get isDefault(): boolean {
|
||||
if (this.complexityData && this.serviceType === PolicyComponentServiceType.MGMT) {
|
||||
return (this.complexityData as PasswordComplexityPolicyView.AsObject).pb_default;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ 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 { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
@@ -29,6 +30,7 @@ import { PasswordComplexityPolicyComponent } from './password-complexity-policy.
|
||||
MatTooltipModule,
|
||||
TranslateModule,
|
||||
DetailLayoutModule,
|
||||
MatProgressSpinnerModule,
|
||||
],
|
||||
})
|
||||
export class PasswordComplexityPolicyModule { }
|
||||
|
@@ -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 { }
|
@@ -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>
|
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,7 +1,6 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { PolicyComponentAction } from '../policy-component-action.enum';
|
||||
import { PasswordLockoutPolicyComponent } from './password-lockout-policy.component';
|
||||
|
||||
const routes: Routes = [
|
||||
@@ -10,15 +9,6 @@ const routes: Routes = [
|
||||
component: PasswordLockoutPolicyComponent,
|
||||
data: {
|
||||
animation: 'DetailPage',
|
||||
action: PolicyComponentAction.MODIFY,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'create',
|
||||
component: PasswordLockoutPolicyComponent,
|
||||
data: {
|
||||
animation: 'DetailPage',
|
||||
action: PolicyComponentAction.CREATE,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
@@ -1,19 +1,17 @@
|
||||
<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}}
|
||||
<app-detail-layout [backRouterLink]="[ serviceType === PolicyComponentServiceType.ADMIN ? '/iam' : '/org']"
|
||||
[title]="'POLICY.PWD_LOCKOUT.TITLE' | translate" [description]="'POLICY.PWD_LOCKOUT.DESCRIPTION' | translate">
|
||||
<p class="default" *ngIf="isDefault"> {{'POLICY.DEFAULTLABEL' | translate}}</p>
|
||||
|
||||
<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>
|
||||
</ng-template>
|
||||
|
||||
<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">
|
||||
<span class="left-desc">{{'ORG.POLICY.DATA.MAXATTEMPTS' | translate}}</span>
|
||||
<span class="left-desc">{{'POLICY.DATA.MAXATTEMPTS' | translate}}</span>
|
||||
<span class="fill-space"></span>
|
||||
<div class="length-wrapper">
|
||||
<button mat-icon-button (click)="incrementMaxAttempts()">
|
||||
@@ -26,10 +24,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<mat-slide-toggle color="primary" name="showLockOutFailures" ngDefaultControl
|
||||
[(ngModel)]="lockoutData.showLockOutFailures">
|
||||
<mat-slide-toggle color="primary" name="showLockoutFailure" ngDefaultControl
|
||||
[(ngModel)]="lockoutData.showLockoutFailure">
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
|
||||
button {
|
||||
border-radius: .5rem;
|
||||
.default {
|
||||
color: #5282c1;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
@@ -12,10 +12,9 @@ button {
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: .5rem 0;
|
||||
padding: .3rem 0;
|
||||
|
||||
.left-desc {
|
||||
color: #8795a1;
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
@@ -39,6 +38,5 @@ button {
|
||||
margin-top: 3rem;
|
||||
display: block;
|
||||
padding: .5rem 4rem;
|
||||
border-radius: .5rem;
|
||||
}
|
||||
}
|
||||
|
@@ -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 { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ActivatedRoute } 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 { DefaultPasswordLockoutPolicyView } from 'src/app/proto/generated/admin_pb';
|
||||
import { PasswordLockoutPolicyView } from 'src/app/proto/generated/management_pb';
|
||||
import { AdminService } from 'src/app/services/admin.service';
|
||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
import { PolicyComponentAction } from '../policy-component-action.enum';
|
||||
import { PolicyComponentServiceType } from '../policy-component-types.enum';
|
||||
|
||||
@Component({
|
||||
selector: 'app-password-lockout-policy',
|
||||
@@ -20,37 +17,35 @@ import { PolicyComponentAction } from '../policy-component-action.enum';
|
||||
styleUrls: ['./password-lockout-policy.component.scss'],
|
||||
})
|
||||
export class PasswordLockoutPolicyComponent implements OnDestroy {
|
||||
public title: string = '';
|
||||
public desc: string = '';
|
||||
@Input() public service!: ManagementService | AdminService;
|
||||
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
|
||||
|
||||
componentAction: PolicyComponentAction = PolicyComponentAction.CREATE;
|
||||
|
||||
public PolicyComponentAction: any = PolicyComponentAction;
|
||||
|
||||
public lockoutForm!: FormGroup;
|
||||
public lockoutData!: PasswordLockoutPolicy.AsObject;
|
||||
public lockoutData!: PasswordLockoutPolicyView.AsObject;
|
||||
private sub: Subscription = new Subscription();
|
||||
public PolicyComponentServiceType: any = PolicyComponentServiceType;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private mgmtService: ManagementService,
|
||||
private router: Router,
|
||||
private toast: ToastService,
|
||||
private injector: Injector,
|
||||
) {
|
||||
this.sub = this.route.data.pipe(switchMap(data => {
|
||||
this.componentAction = data.action;
|
||||
return this.route.params;
|
||||
})).subscribe(params => {
|
||||
this.title = 'ORG.POLICY.PWD_LOCKOUT.TITLECREATE';
|
||||
this.desc = 'ORG.POLICY.PWD_LOCKOUT.DESCRIPTIONCREATE';
|
||||
this.serviceType = data.serviceType;
|
||||
|
||||
if (this.componentAction === PolicyComponentAction.MODIFY) {
|
||||
this.getData(params).then(data => {
|
||||
if (data) {
|
||||
this.lockoutData = data.toObject() as PasswordLockoutPolicy.AsObject;
|
||||
}
|
||||
});
|
||||
switch (this.serviceType) {
|
||||
case PolicyComponentServiceType.MGMT:
|
||||
this.service = this.injector.get(ManagementService as Type<ManagementService>);
|
||||
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();
|
||||
}
|
||||
|
||||
private async getData(params: any):
|
||||
Promise<PasswordLockoutPolicy | PasswordAgePolicy | PasswordComplexityPolicy | OrgIamPolicy | undefined> {
|
||||
|
||||
this.title = 'ORG.POLICY.PWD_LOCKOUT.TITLE';
|
||||
this.desc = 'ORG.POLICY.PWD_LOCKOUT.DESCRIPTION';
|
||||
return this.mgmtService.GetPasswordLockoutPolicy();
|
||||
private fetchData(): void {
|
||||
this.getData().then(data => {
|
||||
if (data) {
|
||||
this.lockoutData = data.toObject() as PasswordLockoutPolicyView.AsObject;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public deletePolicy(): void {
|
||||
this.mgmtService.DeletePasswordLockoutPolicy(this.lockoutData.id).then(() => {
|
||||
this.toast.showInfo('Successfully deleted');
|
||||
}).catch(error => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
private getData(): Promise<PasswordLockoutPolicyView | DefaultPasswordLockoutPolicyView> {
|
||||
switch (this.serviceType) {
|
||||
case PolicyComponentServiceType.MGMT:
|
||||
return (this.service as ManagementService).GetPasswordLockoutPolicy();
|
||||
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 {
|
||||
@@ -87,27 +94,44 @@ export class PasswordLockoutPolicyComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
public savePolicy(): void {
|
||||
if (this.componentAction === PolicyComponentAction.CREATE) {
|
||||
this.mgmtService.CreatePasswordLockoutPolicy(
|
||||
this.lockoutData.description,
|
||||
let promise: Promise<any>;
|
||||
if (this.service instanceof AdminService) {
|
||||
promise = this.service.UpdateDefaultPasswordLockoutPolicy(
|
||||
this.lockoutData.maxAttempts,
|
||||
this.lockoutData.showLockOutFailures,
|
||||
this.lockoutData.showLockoutFailure,
|
||||
).then(() => {
|
||||
this.router.navigate(['org']);
|
||||
this.toast.showInfo('POLICY.TOAST.SET', true);
|
||||
}).catch(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(
|
||||
this.lockoutData.description,
|
||||
this.lockoutData.maxAttempts,
|
||||
this.lockoutData.showLockOutFailures,
|
||||
).then(() => {
|
||||
this.router.navigate(['org']);
|
||||
}).catch(error => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
public get isDefault(): boolean {
|
||||
if (this.lockoutData && this.serviceType === PolicyComponentServiceType.MGMT) {
|
||||
return (this.lockoutData as PasswordLockoutPolicyView.AsObject).pb_default;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +0,0 @@
|
||||
|
||||
export enum PolicyComponentAction {
|
||||
CREATE = 'create',
|
||||
MODIFY = 'modify',
|
||||
}
|
@@ -4,6 +4,7 @@ export enum PolicyComponentType {
|
||||
COMPLEXITY = 'complexity',
|
||||
IAM = 'iam',
|
||||
LOGIN = 'login',
|
||||
LABEL = 'label',
|
||||
}
|
||||
export enum PolicyComponentServiceType {
|
||||
MGMT = 'mgmt',
|
||||
|
109
console/src/app/modules/policy-grid/policy-grid.component.html
Normal file
109
console/src/app/modules/policy-grid/policy-grid.component.html
Normal 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>
|
@@ -3,17 +3,17 @@ h1 {
|
||||
}
|
||||
|
||||
.top-desc {
|
||||
color: #8795a1;
|
||||
color: var(--grey);
|
||||
}
|
||||
|
||||
.row-lyt {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 -1rem;
|
||||
margin: 0 -.5rem;
|
||||
|
||||
.p-item {
|
||||
flex-basis: 300px;
|
||||
margin: 1rem;
|
||||
flex-basis: 290px;
|
||||
margin: .5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 200px;
|
||||
@@ -38,6 +38,7 @@ h1 {
|
||||
font-size: 2.5rem;
|
||||
height: 2.5rem;
|
||||
line-height: 2.5rem;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +47,7 @@ h1 {
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
font-size: 1.2rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.icon {
|
||||
@@ -56,8 +57,8 @@ h1 {
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: .9rem;
|
||||
color: #8795a1;
|
||||
font-size: 14px;
|
||||
color: var(--grey);
|
||||
}
|
||||
|
||||
.fill-space {
|
||||
@@ -70,7 +71,6 @@ h1 {
|
||||
|
||||
button {
|
||||
margin-right: 1rem;
|
||||
border-radius: .5rem;
|
||||
}
|
||||
}
|
||||
}
|
19
console/src/app/modules/policy-grid/policy-grid.component.ts
Normal file
19
console/src/app/modules/policy-grid/policy-grid.component.ts
Normal 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() { }
|
||||
}
|
27
console/src/app/modules/policy-grid/policy-grid.module.ts
Normal file
27
console/src/app/modules/policy-grid/policy-grid.module.ts
Normal 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 { }
|
@@ -1,96 +1,26 @@
|
||||
<app-detail-layout *ngIf="project" [backRouterLink]="[ '/projects', project?.projectId]"
|
||||
title="{{projectName}} {{ 'PROJECT.MEMBER.TITLE' | translate }}"
|
||||
description="{{ 'PROJECT.MEMBER.DESCRIPTION' | translate }}">
|
||||
<app-refresh-table *ngIf="project" (refreshed)="changePage()" [dataSize]="dataSource.totalResult"
|
||||
[timestamp]="dataSource.viewTimestamp" [selection]="selection" [loading]="dataSource?.loading$ | async">
|
||||
<ng-template appHasRole actions
|
||||
<app-members-table *ngIf="project" [dataSource]="dataSource" [memberRoleOptions]="memberRoleOptions"
|
||||
(updateRoles)="updateRoles($event.member, $event.change)" [factoryLoadFunc]="changePageFactory"
|
||||
(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']">
|
||||
<button (click)="removeProjectMemberSelection()" color="warn"
|
||||
matTooltip="{{'ORG_DETAIL.TABLE.DELETE' | translate}}" class="icon-button" mat-icon-button
|
||||
*ngIf="selection.hasValue()">
|
||||
matTooltip="{{'ORG_DETAIL.TABLE.DELETE' | translate}}" class="del-button" mat-raised-button>
|
||||
<i class="las la-trash"></i>
|
||||
{{'ACTIONS.SELECTIONDELETE' | translate}}
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template appHasRole actions
|
||||
<ng-template appHasRole writeactions
|
||||
[appHasRole]="['project.member.write:'+project.projectId,'project.member.write']">
|
||||
<a color="primary" [disabled]="disabled" class="add-button" (click)="openAddMember()" color="primary"
|
||||
mat-raised-button>
|
||||
<a color="primary" (click)="openAddMember()" color="primary" mat-raised-button>
|
||||
<mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }}
|
||||
</a>
|
||||
</ng-template>
|
||||
|
||||
<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 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>
|
||||
</app-members-table>
|
||||
</app-detail-layout>
|
||||
<!-- TODO: check for both project.member and project.grant.member permissions -->
|
@@ -1,50 +1,7 @@
|
||||
.icon-button {
|
||||
.del-button {
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
border-radius: .5rem;
|
||||
}
|
||||
|
||||
.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;
|
||||
:root {
|
||||
width: 100%;
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user