feat(console): improve org search performance in create project grant page #11121 (#12057)

<!--
Please inform yourself about the contribution guidelines on submitting a
PR here:
https://github.com/zitadel/zitadel/blob/main/CONTRIBUTING.md#submit-a-pull-request-pr.
Take note of how PR/commit titles should be written and replace the
template texts in the sections below. Don't remove any of the sections.
It is important that the commit history clearly shows what is changed
and why.
Important: By submitting a contribution you agree to the terms from our
Licensing Policy as described here:
https://github.com/zitadel/zitadel/blob/main/LICENSING.md#community-contributions.
-->

# Which Problems Are Solved

When creating a new Project Grant in the Zitadel console, the UI becomes
extremely sluggish/unusable. The issue appears tied to the organization
search dropdown attempting to load a very large dataset.

# How the Problems Are Solved

The refactored `SearchOrgAutocompleteComponent` uses server side
filtering and ondemand loading using infinite scrolling. This makes sure
on a subset of organizations is ever loaded on the client.

# Additional Changes

- Added tailwindcss for easier styling
- Removed build warnings
- Removed some of the SCSS syntax warnings

# Additional Context
- Closes #11121

---------

Co-authored-by: Max Peintner <[email protected]>
This commit is contained in:
Ramon
2026-04-22 11:46:27 +02:00
committed by GitHub
co-authored by Max Peintner
parent bfffd69d60
commit dd9dcf9f72
79 changed files with 517 additions and 582 deletions
+1 -1
View File
@@ -101,7 +101,7 @@
"node-forge": "^1.4.0",
"nodemon": "^3.1.14",
"postcss": "^8.5.8",
"prettier": "^3.8.1",
"prettier": "^3.8.3",
"prettier-plugin-organize-imports": "^4.3.0",
"prettier-plugin-tailwindcss": "^0.7.2",
"sass": "^1.98.0",
+5
View File
@@ -0,0 +1,5 @@
{
"plugins": {
"@tailwindcss/postcss": {}
}
}
+10 -35
View File
@@ -22,9 +22,7 @@
"base": "dist/console"
},
"index": "src/index.html",
"polyfills": [
"zone.js"
],
"polyfills": ["zone.js"],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
@@ -37,23 +35,13 @@
"output": "assets/docs/img/tech"
}
],
"styles": [
"src/styles.scss"
],
"scripts": [
"./node_modules/tinycolor2/dist/tinycolor-min.js"
],
"styles": ["src/styles.scss", "src/tailwind.css"],
"scripts": ["./node_modules/tinycolor2/dist/tinycolor-min.js"],
"stylePreprocessorOptions": {
"sass": {
"silenceDeprecations": [
"import",
"mixed-decls"
]
"silenceDeprecations": ["import", "mixed-decls"]
},
"includePaths": [
"node_modules",
"."
]
"includePaths": ["node_modules", "."]
},
"allowedCommonJsDependencies": [
"opentype.js",
@@ -158,29 +146,18 @@
"test": {
"builder": "@angular/build:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"polyfills": ["zone.js", "zone.js/testing"],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"assets": ["src/favicon.ico", "src/assets"],
"styles": ["src/styles.scss"],
"scripts": []
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
"lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
}
}
}
@@ -188,9 +165,7 @@
},
"cli": {
"analytics": false,
"schematicCollections": [
"@angular-eslint/schematics"
],
"schematicCollections": ["@angular-eslint/schematics"],
"packageManager": "pnpm"
},
"schematics": {
+4 -1
View File
@@ -75,6 +75,7 @@
"@types/opentype.js": "^1.3.9",
"@types/qrcode": "^1.5.6",
"@types/uuid": "^11.0.0",
"@tailwindcss/postcss": "^4.2.2",
"@typescript-eslint/eslint-plugin": "^8.58.0",
"@typescript-eslint/parser": "^8.58.0",
"@zitadel/client": "workspace:*",
@@ -92,8 +93,10 @@
"karma-coverage-istanbul-reporter": "^3.0.3",
"karma-jasmine": "^5.1.0",
"karma-jasmine-html-reporter": "^2.2.0",
"prettier": "^3.8.1",
"postcss": "^8.5.8",
"prettier": "^3.8.3",
"prettier-plugin-organize-imports": "^4.3.0",
"tailwindcss": "^4.2.2",
"typescript": "5.9.2"
}
}
@@ -13,8 +13,8 @@
$list-background-color: map-get($background, 300);
$card-background-color: map-get($background, cards);
$border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
$border-selected-color: if($is-dark-theme, #fff, #000);
$border-color: if(sass($is-dark-theme): rgba(#8795a1, 0.2) ; else: rgba(#8795a1, 0.2));
$border-selected-color: if(sass($is-dark-theme): #fff; else: #000);
.full-width {
width: 100%;
@@ -58,11 +58,11 @@
}
.dark-only {
display: if($is-dark-theme, block, none);
display: if(sass($is-dark-theme): block; else: none);
}
.light-only {
display: if($is-dark-theme, none, block);
display: if(sass($is-dark-theme): none; else: block);
}
}
}
@@ -13,8 +13,8 @@
$list-background-color: map-get($background, 300);
$card-background-color: map-get($background, cards);
$border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
$border-selected-color: if($is-dark-theme, #fff, #000);
$border-color: if(sass($is-dark-theme): rgba(#8795a1, 0.2) ; else: rgba(#8795a1, 0.2));
$border-selected-color: if(sass($is-dark-theme): #fff; else: #000);
.framework-change-wrapper {
.framework-card-wrapper {
@@ -36,7 +36,7 @@
align-items: center;
justify-content: center;
width: fit-content;
// background-color: if($is-dark-theme, map-get($background, state), #e4e7e4);
// background-color: if(sass($is-dark-theme): map-get($background, state); else: #e4e7e4);
// box-shadow: 0 0 3px #0000001a;
border: 1px solid rgba(#8795a1, 0.2);
padding: 0 0.5rem;
@@ -51,11 +51,11 @@
}
.dark-only {
display: if($is-dark-theme, block, none);
display: if(sass($is-dark-theme): block; else: none);
}
.light-only {
display: if($is-dark-theme, none, block);
display: if(sass($is-dark-theme): none; else: block);
}
span {
@@ -21,10 +21,10 @@
<span class="left cnsl-secondary-text">
{{ 'APP.GRANT' | translate }}
</span>
@if (configuration.grantTypesList && configuration.grantTypesList.length > 0) {
@if (configuration.grantTypesList && configuration.grantTypesList.length) {
<span class="right">
[
@for (element of configuration.grantTypesList ?? []; track element; let i = $index) {
@for (element of configuration.grantTypesList; track element; let i = $index) {
<span>
{{ 'APP.OIDC.GRANT.' + element | translate }}
{{ i < configuration.grantTypesList.length - 1 ? ', ' : '' }}
@@ -38,10 +38,10 @@
<span class="left cnsl-secondary-text">
{{ 'APP.OIDC.RESPONSETYPE' | translate }}
</span>
@if (configuration.responseTypesList && configuration.responseTypesList.length > 0) {
@if (configuration.responseTypesList && configuration.responseTypesList.length) {
<span class="right">
[
@for (element of configuration.responseTypesList ?? []; track element; let i = $index) {
@for (element of configuration.responseTypesList; track element; let i = $index) {
<span>
{{ 'APP.OIDC.RESPONSE.' + element | translate }}
{{ i < configuration.responseTypesList.length - 1 ? ', ' : '' }}
@@ -13,8 +13,8 @@
$list-background-color: map-get($background, 300);
$card-background-color: map-get($background, cards);
$border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
$border-selected-color: if($is-dark-theme, #fff, #000);
$border-color: if(sass($is-dark-theme): rgba(#8795a1, 0.2) ; else: rgba(#8795a1, 0.2));
$border-selected-color: if(sass($is-dark-theme): #fff; else: #000);
.quickstart-header {
display: flex;
@@ -86,11 +86,11 @@
}
.dark-only {
display: if($is-dark-theme, block, none);
display: if(sass($is-dark-theme): block; else: none);
}
.light-only {
display: if($is-dark-theme, none, block);
display: if(sass($is-dark-theme): none; else: block);
}
span {
@@ -6,9 +6,9 @@ import { Directive, ElementRef, EventEmitter, HostListener, Output } from '@angu
})
export class ScrollableDirective {
// when using this directive, add overflow-y scroll to css
@Output() scrollPosition: EventEmitter<any> = new EventEmitter();
@Output() scrollPosition: EventEmitter<'bottom' | 'top'> = new EventEmitter();
constructor(public el: ElementRef) {}
constructor(private readonly el: ElementRef) {}
@HostListener('scroll', ['$event'])
public onScroll(event: any): void {
@@ -11,7 +11,7 @@
$foreground: map-get($theme, foreground);
$is-dark-theme: map-get($theme, is-dark);
$back: map-get($background, background);
$border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
$border-color: if(sass($is-dark-theme): rgba(#8795a1, 0.2) ; else: rgba(#8795a1, 0.2));
$secondary-text: map-get($foreground, secondary-text);
.accounts-card {
@@ -62,7 +62,7 @@
.action-keys-row {
.key-overlay {
z-index: 0;
background: if($is-dark-theme, #fff, #000);
background: if(sass($is-dark-theme): #fff; else: #000);
opacity: 0.15;
}
}
@@ -3,7 +3,7 @@
$primary-color: map-get($primary, 500);
$is-dark-theme: map-get($theme, is-dark);
$background: map-get($theme, background);
$border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
$border-color: if(sass($is-dark-theme): rgba(#8795a1, 0.2) ; else: rgba(#8795a1, 0.2));
.auth-method-radio-button-wrapper {
display: flex;
@@ -29,7 +29,7 @@
box-sizing: border-box;
&:focus {
border: 1px solid if($is-dark-theme, white, black);
border: 1px solid if(sass($is-dark-theme): white; else: black);
}
}
@@ -39,7 +39,7 @@
box-shadow: 0 0 6px rgb(0 0 0 / 10%);
.cnsl-radio-header span {
color: if($is-dark-theme, white, white);
color: if(sass($is-dark-theme): white; else: white);
}
}
@@ -108,7 +108,7 @@
span {
margin: 2rem;
font-size: 30px;
color: if($is-dark-theme, #00000050, #ffffff50);
color: if(sass($is-dark-theme): #00000050; else: #ffffff50);
}
}
@@ -11,7 +11,7 @@
$primary-color: map-get($primary, 500);
$is-dark-theme: map-get($theme, is-dark);
$background: map-get($theme, background);
$border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
$border-color: if(sass($is-dark-theme): rgba(#8795a1, 0.2) ; else: rgba(#8795a1, 0.2));
input[type='radio'].app {
appearance: none;
@@ -20,7 +20,7 @@
box-sizing: border-box;
&:focus {
border: 1px solid if($is-dark-theme, white, black);
border: 1px solid if(sass($is-dark-theme): white; else: black);
}
}
@@ -31,7 +31,7 @@
box-shadow: 0 0 6px rgb(0 0 0 / 10%);
.cnsl-type-radio-header span {
color: if($is-dark-theme, white, white);
color: if(sass($is-dark-theme): white; else: white);
}
}
@@ -64,7 +64,7 @@
span {
margin: 2rem;
font-size: 30px;
color: if($is-dark-theme, #00000050, #ffffff50);
color: if(sass($is-dark-theme): #00000050; else: #ffffff50);
}
}
+3 -3
View File
@@ -4,8 +4,8 @@
$background: map-get($theme, background);
$card-background-color: map-get($background, cards);
$is-dark-theme: map-get($theme, is-dark);
$border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
$border-selected-color: if($is-dark-theme, #fff, #000);
$border-color: if(sass($is-dark-theme): rgba(#8795a1, 0.2) ; else: rgba(#8795a1, 0.2));
$border-selected-color: if(sass($is-dark-theme): #fff; else: #000);
.card {
background-color: $card-background-color;
@@ -57,7 +57,7 @@
.cnsl-chip {
height: auto;
border: 1px solid $border-color;
background: if($is-dark-theme, map-get($background, state), #e4e7e4) !important;
background: if(sass($is-dark-theme): map-get($background, state) ; else: #e4e7e4) !important;
.cnsl-chip-content {
display: flex;
@@ -1,6 +1,6 @@
@mixin domain-verification-theme($theme) {
$is-dark-theme: map-get($theme, is-dark);
$border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
$border-color: if(sass($is-dark-theme): rgba(#8795a1, 0.2) ; else: rgba(#8795a1, 0.2));
.entry {
margin: 0.5rem 0;
@@ -24,7 +24,7 @@
min-height: 80px;
&.defaulttext {
color: if($is-dark-theme, #ffffff50, #00000050);
color: if(sass($is-dark-theme): #ffffff50; else: #00000050);
}
}
@@ -18,7 +18,7 @@
$foreground: map-get($theme, foreground);
$is-dark-theme: map-get($theme, is-dark);
$card-background-color: map-get($background, cards);
$border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
$border-color: if(sass($is-dark-theme): rgba(#8795a1, 0.2) ; else: rgba(#8795a1, 0.2));
$primary: map-get($theme, primary);
$primary-color: map-get($primary, 500);
@@ -9,8 +9,8 @@
$secondary-text: map-get($foreground, secondary-text);
$is-dark-theme: map-get($theme, is-dark);
$link-hover-color: if($is-dark-theme, map-get($primary, 200), $primary-color);
$link-color: if($is-dark-theme, $lighter-primary-color, $primary-color);
$link-hover-color: if(sass($is-dark-theme): map-get($primary, 200) ; else: $primary-color);
$link-color: if(sass($is-dark-theme): $lighter-primary-color; else: $primary-color);
.filter-events-wrapper {
border-radius: 0.5rem;
@@ -38,7 +38,7 @@
justify-content: space-between;
align-items: center;
padding: 0 0.5rem 0.5rem 0.5rem;
border-bottom: 2px solid if($is-dark-theme, #ffffff15, #00000015);
border-bottom: 2px solid if(sass($is-dark-theme): #ffffff15; else: #00000015);
.filter-middle {
margin: 0 1rem;
@@ -73,7 +73,7 @@
align-items: center;
justify-content: space-around;
padding: 0 0.5rem;
background-color: if($is-dark-theme, #00000020, #00000008);
background-color: if(sass($is-dark-theme): #00000020; else: #00000008);
margin: 0 -0.5rem;
.aggregate-type-select .mat-mdc-select {
@@ -11,7 +11,7 @@
$foreground: map-get($theme, foreground);
$is-dark-theme: map-get($theme, is-dark);
$back: map-get($background, background);
$border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
$border-color: if(sass($is-dark-theme): rgba(#8795a1, 0.2) ; else: rgba(#8795a1, 0.2));
.cnsl-filter-button-wrapper {
position: relative;
@@ -48,7 +48,7 @@
justify-content: space-between;
align-items: center;
padding-bottom: 0.5rem;
border-bottom: 2px solid if($is-dark-theme, #ffffff15, #00000015);
border-bottom: 2px solid if(sass($is-dark-theme): #ffffff15; else: #00000015);
.filter-middle {
margin: 0 1rem;
@@ -74,7 +74,7 @@
align-items: center;
justify-content: space-between;
padding: 0 0.5rem;
background-color: if($is-dark-theme, #00000020, #00000008);
background-color: if(sass($is-dark-theme): #00000020; else: #00000008);
margin: 0 -0.5rem;
.nomethod {
@@ -13,7 +13,7 @@
$is-dark-theme: map-get($theme, is-dark);
$back: map-get($background, background);
$secondary-text: map-get($foreground, secondary-text);
$link-hover-color: if($is-dark-theme, $lighter-primary-color, $darker-primary-color);
$link-hover-color: if(sass($is-dark-theme): $lighter-primary-color; else: $darker-primary-color);
.footer-wrapper {
padding: 2rem;
@@ -71,7 +71,7 @@
.swisssvg {
width: 14rem;
margin: 1rem 0;
color: if($is-dark-theme, #ffffff60, #00000060);
color: if(sass($is-dark-theme): #ffffff60; else: #00000060);
.cross {
fill: white;
@@ -64,7 +64,7 @@
// z-index: 502;
&:hover {
background-color: if($is-dark-theme, #ffffff10, #00000010);
background-color: if(sass($is-dark-theme): #ffffff10; else: #00000010);
}
@media only screen and (max-width: 700px) {
@@ -97,7 +97,7 @@
transition: all ease 0.2s;
&:hover {
background-color: if($is-dark-theme, #ffffff10, #00000010);
background-color: if(sass($is-dark-theme): #ffffff10; else: #00000010);
}
.svgspan {
@@ -146,7 +146,7 @@
}
&:hover {
background-color: if($is-dark-theme, #ffffff10, #00000010);
background-color: if(sass($is-dark-theme): #ffffff10; else: #00000010);
}
.mob {
@@ -274,7 +274,7 @@
border-radius: 50%;
transition: border ease 0.15s;
padding: 0;
border: 2px solid if($is-dark-theme, #ffffff80, #00000010);
border: 2px solid if(sass($is-dark-theme): #ffffff80; else: #00000010);
outline: none;
background: none;
@@ -284,7 +284,7 @@
&:hover,
&:focus {
border: 2px solid if($is-dark-theme, #fff, map-get($primary, 200));
border: 2px solid if(sass($is-dark-theme): #fff; else: map-get($primary, 200));
}
.docs {
@@ -234,7 +234,7 @@
</table>
</div>
@if ((loading$ | async) === false && !dataSource?.data?.length) {
@if ((loading$ | async) === false && !dataSource.data.length) {
<div class="no-content-row">
<i class="las la-exclamation"></i>
<span>{{ 'IDP.EMPTY' | translate }}</span>
@@ -26,11 +26,11 @@
}
&.dark {
display: if($is-dark-theme, block, none);
display: if(sass($is-dark-theme): block; else: none);
}
&.light {
display: if($is-dark-theme, none, block);
display: if(sass($is-dark-theme): none; else: block);
}
}
@@ -12,7 +12,7 @@
$back: map-get($background, background);
.cnsl-overlay-backdrop {
background-color: if($is-dark-theme, #00000080, #ffffff80);
background-color: if(sass($is-dark-theme): #00000080; else: #ffffff80);
backdrop-filter: blur(2px);
}
@@ -25,7 +25,7 @@
padding: 1rem;
font-size: 14px;
width: 300px;
border: 1px solid if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
border: 1px solid if(sass($is-dark-theme): rgba(#8795a1, 0.2) ; else: rgba(#8795a1, 0.2));
&::before {
visibility: hidden;
@@ -2,7 +2,7 @@
<div class="info-row">
<div class="info-wrapper">
<p class="info-row-title">{{ 'USER.PAGES.STATE' | translate }}</p>
@if (user?.state) {
@if (user.state) {
<p
class="state"
[ngClass]="{
@@ -60,7 +60,7 @@
<div class="info-row">
<div class="info-wrapper">
<p class="info-row-title">{{ 'IAM.PAGES.STATE' | translate }}</p>
@if (instance?.state) {
@if (instance.state) {
<p
class="state"
[ngClass]="{
@@ -74,25 +74,25 @@
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'INSTANCEID' | translate }}</p>
@if (instance?.id) {
@if (instance.id) {
<p class="info-row-desc">{{ instance.id }}</p>
}
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'NAME' | translate }}</p>
@if (instance?.name) {
@if (instance.name) {
<p class="info-row-desc">{{ instance.name }}</p>
}
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'VERSION' | translate }}</p>
@if (instance?.version) {
@if (instance.version) {
<p class="info-row-desc">{{ instance.version }}</p>
}
</div>
<div class="info-wrapper width">
<p class="info-row-title">{{ 'IAM.PAGES.DOMAINLIST' | translate }}</p>
@for (domain of instance?.domainsList; track domain) {
@for (domain of instance.domainsList; track domain) {
<div class="copy-row">
<button
[disabled]="copied === domain.domain"
@@ -108,7 +108,7 @@
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'ORG.PAGES.CREATIONDATE' | translate }}</p>
@if (instance?.details?.creationDate; as creationDate) {
@if (instance.details?.creationDate; as creationDate) {
<p class="info-row-desc">
{{ creationDate | timestampToDate | localizedDate: 'dd. MMMM yyyy, HH:mm' }}
</p>
@@ -116,7 +116,7 @@
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'ORG.PAGES.DATECHANGED' | translate }}</p>
@if (instance?.details?.changeDate; as changeDate) {
@if (instance.details?.changeDate; as changeDate) {
<p class="info-row-desc">
{{ changeDate | timestampToDate | localizedDate: 'dd. MMMM yyyy, HH:mm' }}
</p>
@@ -129,7 +129,7 @@
<div class="info-row">
<div class="info-wrapper">
<p class="info-row-title">{{ 'ORG.PAGES.STATE' | translate }}</p>
@if (org?.state) {
@if (org.state) {
<p
class="state"
[ngClass]="{
@@ -143,7 +143,7 @@
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'ORG.PAGES.ID' | translate }}</p>
@if (org?.id) {
@if (org.id) {
<p class="info-row-desc">{{ org.id }}</p>
}
</div>
@@ -165,7 +165,7 @@
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'ORG.PAGES.CREATIONDATE' | translate }}</p>
@if (org?.details?.creationDate; as creationDate) {
@if (org.details?.creationDate; as creationDate) {
<p class="info-row-desc">
{{ creationDate | timestampToDate | localizedDate: 'dd. MMMM yyyy, HH:mm' }}
</p>
@@ -173,7 +173,7 @@
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'ORG.PAGES.DATECHANGED' | translate }}</p>
@if (org?.details?.changeDate; as changeDate) {
@if (org.details?.changeDate; as changeDate) {
<p class="info-row-desc">
{{ changeDate | timestampToDate | localizedDate: 'dd. MMMM yyyy, HH:mm' }}
</p>
@@ -186,7 +186,7 @@
<div class="info-row">
<div class="info-wrapper">
<p class="info-row-title">{{ 'PROJECT.STATE.TITLE' | translate }}</p>
@if (project?.state) {
@if (project.state) {
<p
class="state"
[ngClass]="{
@@ -200,13 +200,13 @@
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'PROJECT.PAGES.ID' | translate }}</p>
@if (project?.id) {
@if (project.id) {
<p class="info-row-desc">{{ project.id }}</p>
}
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'PROJECT.PAGES.CREATEDON' | translate }}</p>
@if (project?.details?.creationDate; as creationDate) {
@if (project.details?.creationDate; as creationDate) {
<p class="info-row-desc">
{{ creationDate | timestampToDate | localizedDate: 'dd. MMMM yyyy, HH:mm' }}
</p>
@@ -214,7 +214,7 @@
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'PROJECT.PAGES.LASTMODIFIED' | translate }}</p>
@if (project?.details?.changeDate; as changeDate) {
@if (project.details?.changeDate; as changeDate) {
<p class="info-row-desc">
{{ changeDate | timestampToDate | localizedDate: 'dd. MMMM yyyy, HH:mm' }}
</p>
@@ -227,7 +227,7 @@
<div class="info-row">
<div class="info-wrapper">
<p class="info-row-title">{{ 'PROJECT.STATE.TITLE' | translate }}</p>
@if (grantedProject?.state) {
@if (grantedProject.state) {
<p
class="state"
[ngClass]="{
@@ -241,19 +241,19 @@
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'PROJECT.PAGES.ID' | translate }}</p>
@if (grantedProject?.projectId) {
@if (grantedProject.projectId) {
<p class="info-row-desc">{{ grantedProject.projectId }}</p>
}
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'PROJECT.GRANT.GRANTID' | translate }}</p>
@if (grantedProject?.grantId) {
@if (grantedProject.grantId) {
<p class="info-row-desc">{{ grantedProject.grantId }}</p>
}
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'PROJECT.PAGES.CREATEDON' | translate }}</p>
@if (grantedProject?.details?.creationDate; as creationDate) {
@if (grantedProject.details?.creationDate; as creationDate) {
<p class="info-row-desc">
{{ creationDate | timestampToDate | localizedDate: 'dd. MMMM yyyy, HH:mm' }}
</p>
@@ -261,7 +261,7 @@
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'PROJECT.PAGES.LASTMODIFIED' | translate }}</p>
@if (grantedProject?.details?.changeDate; as changeDate) {
@if (grantedProject.details?.changeDate; as changeDate) {
<p class="info-row-desc">
{{ changeDate | timestampToDate | localizedDate: 'dd. MMMM yyyy, HH:mm' }}
</p>
@@ -274,7 +274,7 @@
<div class="info-row">
<div class="info-wrapper">
<p class="info-row-title">{{ 'APP.PAGES.STATE' | translate }}</p>
@if (app?.state) {
@if (app.state) {
<p
class="state"
[ngClass]="{
@@ -286,7 +286,7 @@
</p>
}
</div>
@if (app?.apiConfig?.authMethodType; as authMethodType) {
@if (app.apiConfig?.authMethodType; as authMethodType) {
<div class="info-wrapper">
<p class="info-row-title">{{ 'APP.AUTHMETHOD' | translate }}</p>
<p class="info-row-desc">
@@ -294,7 +294,7 @@
</p>
</div>
}
@if (app?.oidcConfig?.authMethodType; as authMethodType) {
@if (app.oidcConfig?.authMethodType; as authMethodType) {
<div class="info-wrapper">
<p class="info-row-title">{{ 'APP.AUTHMETHOD' | translate }}</p>
<p class="info-row-desc">
@@ -304,13 +304,13 @@
}
<div class="info-wrapper">
<p class="info-row-title">{{ 'APP.PAGES.ID' | translate }}</p>
@if (app?.id) {
@if (app.id) {
<p class="info-row-desc">{{ app.id }}</p>
}
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'APP.PAGES.DATECREATED' | translate }}</p>
@if (app?.details?.creationDate; as creationDate) {
@if (app.details?.creationDate; as creationDate) {
<p class="info-row-desc">
{{ creationDate | timestampToDate | localizedDate: 'dd. MMMM yyyy, HH:mm' }}
</p>
@@ -318,46 +318,44 @@
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'APP.PAGES.DATECHANGED' | translate }}</p>
@if (app?.details?.changeDate; as changeDate) {
@if (app.details?.changeDate; as changeDate) {
<p class="info-row-desc">
{{ changeDate | timestampToDate | localizedDate: 'dd. MMMM yyyy, HH:mm' }}
</p>
}
</div>
@if (app) {
<div class="info-wrapper">
<p class="info-row-title">{{ 'APP.OIDC.INFO.CLIENTID' | translate }}</p>
@if (app.oidcConfig?.clientId) {
<div class="copy-row">
@if (app.oidcConfig?.clientId; as clientId) {
<button
[disabled]="copied === clientId"
[matTooltip]="(copied !== clientId ? 'ACTIONS.COPY' : 'ACTIONS.COPIED') | translate"
cnslCopyToClipboard
[valueToCopy]="clientId"
(copiedValue)="copied = $event"
>
{{ clientId }}
</button>
}
</div>
}
@if (app.apiConfig?.clientId) {
<div class="copy-row">
@if (app.apiConfig?.clientId; as clientId) {
<button
[disabled]="copied === clientId"
[matTooltip]="(copied !== clientId ? 'ACTIONS.COPY' : 'ACTIONS.COPIED') | translate"
cnslCopyToClipboard
[valueToCopy]="clientId"
(copiedValue)="copied = $event"
>
{{ clientId }}
</button>
}
</div>
}
</div>
}
<div class="info-wrapper">
<p class="info-row-title">{{ 'APP.OIDC.INFO.CLIENTID' | translate }}</p>
@if (app.oidcConfig?.clientId) {
<div class="copy-row">
@if (app.oidcConfig?.clientId; as clientId) {
<button
[disabled]="copied === clientId"
[matTooltip]="(copied !== clientId ? 'ACTIONS.COPY' : 'ACTIONS.COPIED') | translate"
cnslCopyToClipboard
[valueToCopy]="clientId"
(copiedValue)="copied = $event"
>
{{ clientId }}
</button>
}
</div>
}
@if (app.apiConfig?.clientId) {
<div class="copy-row">
@if (app.apiConfig?.clientId; as clientId) {
<button
[disabled]="copied === clientId"
[matTooltip]="(copied !== clientId ? 'ACTIONS.COPY' : 'ACTIONS.COPIED') | translate"
cnslCopyToClipboard
[valueToCopy]="clientId"
(copiedValue)="copied = $event"
>
{{ clientId }}
</button>
}
</div>
}
</div>
</div>
}
@@ -104,7 +104,7 @@
}
.keyboard-shortcuts-group {
$border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
$border-color: if(sass($is-dark-theme): rgba(#8795a1, 0.2) ; else: rgba(#8795a1, 0.2));
border: 1px solid $border-color;
background-color: $card-background-color;
@@ -128,7 +128,7 @@
.keyboard-shortcuts-action-key {
.keyboard-shortcuts-key-overlay {
background: if($is-dark-theme, #fff, #000);
background: if(sass($is-dark-theme): #fff; else: #000);
}
}
}
@@ -26,7 +26,7 @@
.cnsl-form-field-disabled {
.cnsl-label-wrapper {
color: if($is-dark-theme, #ffffff80, #00000061);
color: if(sass($is-dark-theme): #ffffff80; else: #00000061);
}
}
@@ -51,7 +51,7 @@
padding: 2px;
margin: -2px -9px -2px 8px;
margin-left: 6px;
background-color: if($is-dark-theme, #ffffff20, #00000020);
background-color: if(sass($is-dark-theme): #ffffff20; else: #00000020);
line-height: 18px;
font-size: 11px;
transition: background 0.2s ease;
@@ -69,7 +69,7 @@
}
&:hover {
background: if($is-dark-theme, #ffffff40, #00000010);
background: if(sass($is-dark-theme): #ffffff40; else: #00000010);
}
&.active {
@@ -78,7 +78,7 @@
.c_label {
.count {
background-color: if($is-dark-theme, #00000020, #ffffff20);
background-color: if(sass($is-dark-theme): #00000020; else: #ffffff20);
}
}
}
@@ -82,7 +82,7 @@
padding: 2px;
margin: -2px -9px -2px 8px;
margin-left: 6px;
background-color: if($is-dark-theme, #ffffff20, #00000020);
background-color: if(sass($is-dark-theme): #ffffff20; else: #00000020);
line-height: 18px;
font-size: 11px;
transition: background 0.2s ease;
@@ -100,7 +100,7 @@
}
&:hover {
background: if($is-dark-theme, #ffffff40, #00000010);
background: if(sass($is-dark-theme): #ffffff40; else: #00000010);
}
&.active {
@@ -110,7 +110,7 @@
.c_label {
.count {
display: inline-block;
background-color: if($is-dark-theme, #00000020, #ffffff20);
background-color: if(sass($is-dark-theme): #00000020; else: #ffffff20);
}
}
}
@@ -136,7 +136,7 @@
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
.slash {
color: if($is-dark-theme, #525454, #d1d5d9);
color: if(sass($is-dark-theme): #525454; else: #d1d5d9);
}
.org-button {
@@ -185,7 +185,7 @@
margin-right: 0.5rem;
width: 100px;
min-width: 50px;
background: if($is-dark-theme, #ffffff20, #00000010);
background: if(sass($is-dark-theme): #ffffff20; else: #00000010);
border: none;
padding: 2px;
border-radius: 50vw;
@@ -193,7 +193,7 @@
display: flex;
&:hover {
background: if($is-dark-theme, #ffffff, $primary-color);
background: if(sass($is-dark-theme): #ffffff; else: $primary-color);
transition: background ease 0.15s;
}
@@ -227,7 +227,7 @@
right: 0;
bottom: 0;
left: 0;
background: if($is-dark-theme, #fff, #000);
background: if(sass($is-dark-theme): #fff; else: #000);
opacity: 0.15;
border-radius: 4px;
}
@@ -70,7 +70,7 @@
width: 1.5rem;
border-radius: 50vw;
margin-right: 1rem;
background-color: if($is-dark-theme, map-get($background, state), #e4e7e4);
background-color: if(sass($is-dark-theme): map-get($background, state) ; else: #e4e7e4);
box-shadow: 0 0 3px #0000001a;
border: 1px solid rgba(#8795a1, 0.2);
@@ -13,8 +13,8 @@
$list-background-color: map-get($background, 300);
$card-background-color: map-get($background, cards);
$border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
$border-selected-color: if($is-dark-theme, #fff, #000);
$border-color: if(sass($is-dark-theme): rgba(#8795a1, 0.2) ; else: rgba(#8795a1, 0.2));
$border-selected-color: if(sass($is-dark-theme): #fff; else: #000);
.onboarding-header {
display: flex;
@@ -141,7 +141,7 @@
width: 1.5rem;
border-radius: 50vw;
margin-right: 1rem;
background-color: if($is-dark-theme, map-get($background, state), #e4e7e4);
background-color: if(sass($is-dark-theme): map-get($background, state) ; else: #e4e7e4);
box-shadow: 0 0 3px #0000001a;
border: 1px solid rgba(#8795a1, 0.2);
@@ -87,11 +87,11 @@
}
&.dark {
display: if($is-dark-theme, block, none);
display: if(sass($is-dark-theme): block; else: none);
}
&.light {
display: if($is-dark-theme, none, block);
display: if(sass($is-dark-theme): none; else: block);
}
}
@@ -69,11 +69,11 @@
}
&.dark {
display: if($is-dark-theme, block, none);
display: if(sass($is-dark-theme): block; else: none);
}
&.light {
display: if($is-dark-theme, none, block);
display: if(sass($is-dark-theme): none; else: block);
}
}
@@ -2,7 +2,7 @@
@use '../../../label/label.component.scss' as label;
@mixin preview-theme($show-dark) {
$theme: if($show-dark, $caos-dark-app-theme, $caos-light-app-theme);
$theme: if(sass($show-dark): $caos-dark-app-theme; else: $caos-light-app-theme);
@include input.input-theme($theme);
@include label.cnsl-label-theme($theme);
@@ -7,8 +7,8 @@
$is-dark-theme: map.get($theme, is-dark);
$background: map.get($theme, background);
$foreground: map.get($theme, foreground);
$p-border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
$border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
$p-border-color: if(sass($is-dark-theme): rgba(#8795a1, 0.2) ; else: rgba(#8795a1, 0.2));
$border-color: if(sass($is-dark-theme): rgba(#8795a1, 0.2) ; else: rgba(#8795a1, 0.2));
.privatelabeling-policy {
.header {
@@ -13,8 +13,8 @@
$list-background-color: map-get($background, 300);
$card-background-color: map-get($background, cards);
$border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
$border-selected-color: if($is-dark-theme, #fff, #000);
$border-color: if(sass($is-dark-theme): rgba(#8795a1, 0.2) ; else: rgba(#8795a1, 0.2));
$border-selected-color: if(sass($is-dark-theme): #fff; else: #000);
.project-role-chip {
display: flex;
@@ -118,7 +118,7 @@
<tr class="highlight" mat-row *matRowDef="let role; columns: displayedColumns"></tr>
</table>
</div>
@if ((dataSource.loading$ | async) === false && !dataSource?.totalResult) {
@if ((dataSource.loading$ | async) === false && !dataSource.totalResult) {
<div class="no-content-row">
<i class="las la-exclamation"></i>
<span>{{ 'PROJECT.ROLE.EMPTY' | translate }}</span>
@@ -25,11 +25,11 @@
}
&.dark {
display: if($is-dark-theme, block, none);
display: if(sass($is-dark-theme): block; else: none);
}
&.light {
display: if($is-dark-theme, none, block);
display: if(sass($is-dark-theme): none; else: block);
}
}
@@ -1,36 +1,57 @@
<form>
<cnsl-form-field class="full-width">
<cnsl-form-field>
<cnsl-label>{{ 'PROJECT.GRANT.CREATE.SEL_ORG_FORMFIELD' | translate }}</cnsl-label>
<input
cnslInput
type="text"
placeholder="Organization XY"
#nameInput
[formControl]="myControl"
[matAutocomplete]="auto"
data-e2e="add-org-input"
/>
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)" [displayWith]="displayFn">
@if (isLoading) {
<mat-option class="is-loading">
<mat-spinner diameter="30"></mat-spinner>
</mat-option>
<span class="tw:relative">
<input
cnslInput
type="text"
class="tw:max-w-125"
[class.tw:pr-9]="query.isFetching()"
[matAutocomplete]="auto"
[formControl]="searchControl"
name="name"
data-e2e="add-org-input"
#input
(keyup)="searchSignal.set(input.value)"
/>
@if (query.isFetching()) {
<span class="tw:absolute tw:inset-2 tw:flex tw:items-center tw:justify-end">
<mat-spinner diameter="25" color="primary"></mat-spinner>
</span>
}
@for (org of filteredOrgs; track org) {
<mat-option [value]="org">
<div class="org-option" data-e2e="org-option">
<div class="org-option-column">
<span>{{ org.name }}</span>
<span class="fill-space"></span>
<span class="smaller cnsl-secondary-text">{{ org.primaryDomain }}</span>
</div>
</div>
</mat-option>
}
</mat-autocomplete>
<span class="org-autocomplete-target-desc">
{{ 'PROJECT.GRANT.CREATE.SEL_ORG_DESC' | translate }}
</span>
<mat-autocomplete
#auto="matAutocomplete"
[displayWith]="displayOrg"
[requireSelection]="true"
[autoActiveFirstOption]="true"
(optionSelected)="searchControl.value && selectionChanged.emit(searchControl.value)"
>
<div cnslScrollable class="tw:overflow-y-scroll tw:max-h-full" (scrollPosition)="onScroll($event)">
@for (page of query.data()?.pages; track $index) {
@for (org of page.result; track org.id) {
<mat-option
class="tw:max-w-full tw:relative"
[value]="org"
data-e2e="org-option"
[matTooltip]="org.name"
(mouseenter)="showTooltip.set(name.scrollWidth > name.clientWidth)"
(mouseleave)="showTooltip.set(false)"
[matTooltipDisabled]="!showTooltip()"
>
<div
class="tw:absolute tw:inset-0 tw:flex tw:h-full tw:w-full tw:min-w-0 tw:flex-col tw:justify-center"
data-e2e="org-option"
>
<span #name class="tw:block tw:mr-4 tw:ml-4 tw:truncate">{{ org.name }}</span>
<span class="tw:block tw:mr-4 tw:ml-4 tw:truncate tw:text-[13px] tw:text-gray-500">{{
org.primaryDomain
}}</span>
</div>
</mat-option>
}
}
</div>
</mat-autocomplete>
</cnsl-form-field>
</form>
@@ -1,38 +0,0 @@
.full-width {
width: 100%;
}
input {
max-width: 500px;
}
.org-option {
display: flex;
align-items: center;
.org-option-column {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
width: 100%;
span {
line-height: normal;
}
.fill-space {
flex: 1;
}
.smaller {
font-size: 13px;
}
}
}
.org-autocomplete-target-desc {
font-size: 14px;
display: block;
margin-top: 0.5rem;
}
@@ -1,24 +0,0 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { SearchOrgAutocompleteComponent } from './search-org-autocomplete.component';
describe('SearchOrgComponent', () => {
let component: SearchOrgAutocompleteComponent;
let fixture: ComponentFixture<SearchOrgAutocompleteComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [SearchOrgAutocompleteComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SearchOrgAutocompleteComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,85 +1,135 @@
import { Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
import { UntypedFormControl } from '@angular/forms';
import { MatAutocomplete, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { debounceTime, from, map, Subject, switchMap, takeUntil, tap } from 'rxjs';
import { TextQueryMethod } from 'src/app/proto/generated/zitadel/object_pb';
import { Org, OrgNameQuery, OrgQuery, OrgState, OrgStateQuery } from 'src/app/proto/generated/zitadel/org_pb';
import { AuthenticationService } from 'src/app/services/authentication.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ChangeDetectionStrategy, Component, effect, inject, output, signal } from '@angular/core';
import { NewAuthService } from 'src/app/services/new-auth.service';
import { injectInfiniteQuery, keepPreviousData } from '@tanstack/angular-query-experimental';
import { UserService } from 'src/app/services/user.service';
import { AuthService, ListMyProjectOrgsRequestSchema } from '@zitadel/proto/zitadel/auth_pb';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatInputModule } from '@angular/material/input';
import { InputModule } from '../input/input.module';
import { TranslatePipe } from '@ngx-translate/core';
import { TextQueryMethod } from '@zitadel/proto/zitadel/object_pb';
import { ScrollingModule } from '@angular/cdk/scrolling';
import { Org, OrgFieldName, OrgState } from '@zitadel/proto/zitadel/org_pb';
import { MessageInitShape } from '@bufbuild/protobuf';
import { MatTooltip } from '@angular/material/tooltip';
import { requiredValidator } from '../form-field/validators/validators';
import { ScrollableDirective } from 'src/app/directives/scrollable/scrollable.directive';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { debounceTime } from 'rxjs';
import { MatProgressSpinner } from '@angular/material/progress-spinner';
import { ToastService } from 'src/app/services/toast.service';
@Component({
selector: 'cnsl-search-org-autocomplete',
templateUrl: './search-org-autocomplete.component.html',
styleUrls: ['./search-org-autocomplete.component.scss'],
standalone: false,
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
ScrollingModule,
MatInputModule,
MatAutocompleteModule,
ReactiveFormsModule,
InputModule,
TranslatePipe,
FormsModule,
MatTooltip,
ScrollableDirective,
MatProgressSpinner,
],
})
export class SearchOrgAutocompleteComponent implements OnInit, OnDestroy {
public myControl: UntypedFormControl = new UntypedFormControl();
public filteredOrgs: Array<Org.AsObject> = [];
public isLoading: boolean = false;
@ViewChild('auto') public matAutocomplete!: MatAutocomplete;
@Output() public selectionChanged: EventEmitter<Org.AsObject> = new EventEmitter();
export class SearchOrgAutocompleteComponent {
public readonly selectionChanged = output<Org>();
private unsubscribed$: Subject<void> = new Subject();
constructor(
public authService: AuthenticationService,
private auth: GrpcAuthService,
) {
this.myControl.valueChanges
.pipe(
takeUntil(this.unsubscribed$),
debounceTime(200),
tap(() => (this.isLoading = true)),
switchMap((value) => {
const stateQuery = new OrgQuery();
const orgStateQuery = new OrgStateQuery();
orgStateQuery.setState(OrgState.ORG_STATE_ACTIVE);
stateQuery.setStateQuery(orgStateQuery);
protected readonly searchControl = new FormControl<Org | null>(null, { validators: [requiredValidator] });
protected readonly searchSignal = signal('');
protected readonly debouncedSearchSignal = toSignal(toObservable(this.searchSignal).pipe(debounceTime(250)), {
initialValue: this.searchSignal(),
});
let queries: OrgQuery[] = [stateQuery];
private readonly authService = inject(NewAuthService);
private readonly userService = inject(UserService);
if (value) {
const nameQuery = new OrgQuery();
const orgNameQuery = new OrgNameQuery();
orgNameQuery.setName(value);
orgNameQuery.setMethod(TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE);
nameQuery.setNameQuery(orgNameQuery);
queries = [stateQuery, nameQuery];
}
protected readonly query = injectInfiniteQuery(() => {
const search = this.debouncedSearchSignal();
return from(this.auth.listMyProjectOrgs(undefined, 0, queries)).pipe(
map((resp) => {
return resp.resultList.sort((left, right) => left.name.localeCompare(right.name));
}),
);
}),
)
.subscribe((returnValue) => {
this.isLoading = false;
this.filteredOrgs = returnValue;
});
}
const stateQuery = [
{
query: {
case: 'stateQuery',
value: {
state: OrgState.ACTIVE,
},
},
},
] satisfies MessageInitShape<typeof ListMyProjectOrgsRequestSchema>['queries'];
public ngOnInit(): void {
const query = new OrgQuery();
const orgStateQuery = new OrgStateQuery();
orgStateQuery.setState(OrgState.ORG_STATE_ACTIVE);
query.setStateQuery(orgStateQuery);
return {
queryKey: [this.userService.userId(), AuthService.name, AuthService.method.listMyProjectOrgs.name, 'infinite', search],
placeholderData: keepPreviousData,
queryFn: async ({ pageParam, signal }) =>
this.authService.listMyProjectOrgs(
{
query: {
limit: 20,
offset: BigInt(pageParam.offset),
},
sortingColumn: OrgFieldName.NAME,
queries: search
? [
...stateQuery,
{
query: {
case: 'nameQuery',
value: {
name: search,
method: TextQueryMethod.CONTAINS_IGNORE_CASE,
},
},
},
]
: stateQuery,
},
signal,
),
initialPageParam: {
offset: 0,
},
getNextPageParam: (lastPage, allPages, { offset }) => {
const nextPageParam = { offset: offset + lastPage.result.length };
const loadedCount = allPages.reduce((count, page) => count + page.result.length, 0);
this.auth.listMyProjectOrgs(undefined, 0, [query]).then((orgs) => {
this.filteredOrgs = orgs.resultList;
if (loadedCount < lastPage.details!.totalResult) {
return nextPageParam;
}
return undefined;
},
};
});
// used to only open matTooltip if the text gets truncated
protected readonly showTooltip = signal(false);
constructor() {
const toastService = inject(ToastService);
effect(() => {
const error = this.query.error();
if (error) {
toastService.showError(error);
}
});
}
public ngOnDestroy(): void {
this.unsubscribed$.next();
protected async onScroll(position: 'bottom' | 'top') {
if (this.query.isFetchingNextPage() || !this.query.hasNextPage() || position === 'top') {
return;
}
await this.query.fetchNextPage();
}
public displayFn(org?: Org.AsObject): string {
return org ? `${org.name}` : '';
}
public selected(event: MatAutocompleteSelectedEvent): void {
this.selectionChanged.emit(event.option.value);
protected displayOrg(org: Org | null) {
return org?.name ?? '';
}
}
@@ -1,32 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
import { MatChipsModule } from '@angular/material/chips';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
import { TranslateModule } from '@ngx-translate/core';
import { InputModule } from 'src/app/modules/input/input.module';
import { SearchOrgAutocompleteComponent } from './search-org-autocomplete.component';
@NgModule({
declarations: [SearchOrgAutocompleteComponent],
imports: [
CommonModule,
MatAutocompleteModule,
MatChipsModule,
MatButtonModule,
InputModule,
MatIconModule,
ReactiveFormsModule,
MatProgressSpinnerModule,
FormsModule,
TranslateModule,
MatSelectModule,
],
exports: [SearchOrgAutocompleteComponent],
})
export class SearchOrgAutocompleteModule {}
@@ -9,8 +9,8 @@
$secondary-text: map-get($foreground, secondary-text);
$is-dark-theme: map-get($theme, is-dark);
$link-hover-color: if($is-dark-theme, map-get($primary, 200), $primary-color);
$link-color: if($is-dark-theme, $lighter-primary-color, $primary-color);
$link-hover-color: if(sass($is-dark-theme): map-get($primary, 200) ; else: $primary-color);
$link-color: if(sass($is-dark-theme): $lighter-primary-color; else: $primary-color);
.user-autocomplete-found {
margin: 0.5rem 0;
@@ -13,8 +13,8 @@
$list-background-color: map-get($background, 300);
$card-background-color: map-get($background, cards);
$border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
$border-selected-color: if($is-dark-theme, #fff, #000);
$border-color: if(sass($is-dark-theme): rgba(#8795a1, 0.2) ; else: rgba(#8795a1, 0.2));
$border-selected-color: if(sass($is-dark-theme): #fff; else: #000);
.shortcut-container {
width: 100%;
@@ -32,12 +32,12 @@
font-size: 11px;
letter-spacing: 0.05em;
text-transform: uppercase;
color: if($is-dark-theme, #ffffff60, #00000060);
color: if(sass($is-dark-theme): #ffffff60; else: #00000060);
margin-top: 1rem;
font-weight: 600;
margin-bottom: 0.5rem;
padding-top: 1rem;
border-top: 1px solid if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
border-top: 1px solid if(sass($is-dark-theme): rgba(#8795a1, 0.2) ; else: rgba(#8795a1, 0.2));
&:nth-child(2) {
border-top: none;
@@ -131,7 +131,7 @@
</table>
</div>
@if ((loading$ | async) === false && !dataSource?.data?.length) {
@if ((loading$ | async) === false && !dataSource.data.length) {
<div class="no-content-row">
<i class="las la-exclamation"></i>
<span>{{ 'SMTP.LIST.EMPTY' | translate }}</span>
@@ -139,7 +139,6 @@
}
<cnsl-paginator
#paginator
class="paginator"
[timestamp]="configsResult?.details?.viewTimestamp"
[length]="configsResult?.details?.totalResult || 0"
@@ -21,11 +21,11 @@
flex-shrink: 0;
&.dark {
display: if($is-dark-theme, block, none);
display: if(sass($is-dark-theme): block; else: none);
}
&.light {
display: if($is-dark-theme, none, block);
display: if(sass($is-dark-theme): none; else: block);
}
}
@@ -9,7 +9,7 @@
display: flex;
flex-direction: row;
max-width: 400px;
background: if($is-dark-theme, #00000020, map-get($background, cards));
background: if(sass($is-dark-theme): #00000020; else: map-get($background, cards));
margin-left: -1rem;
margin-right: -1rem;
padding: 0 1rem 0.5rem 1rem;
@@ -3,7 +3,7 @@
$foreground: map-get($theme, foreground);
$is-dark-theme: map-get($theme, is-dark);
$card-background-color: map-get($background, cards);
$border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
$border-color: if(sass($is-dark-theme): rgba(#8795a1, 0.2) ; else: rgba(#8795a1, 0.2));
.cnsl-table-action-wrapper {
height: 36px;
@@ -8,7 +8,7 @@
.theme-flex {
display: flex;
align-items: center;
border: 1px solid if($is-dark-theme, #ffffff20, #00000020);
border: 1px solid if(sass($is-dark-theme): #ffffff20; else: #00000020);
border-radius: 50vw;
padding: 0.25rem;
@@ -46,7 +46,7 @@
}
&.active {
background-color: if($is-dark-theme, #ffffff10, #00000010) !important;
background-color: if(sass($is-dark-theme): #ffffff10; else: #00000010) !important;
.moon,
.sun {
opacity: 1;
@@ -196,7 +196,7 @@
></tr>
</table>
</div>
@if ((dataSource.loading$ | async) === false && !dataSource?.totalResult) {
@if ((dataSource.loading$ | async) === false && !dataSource.totalResult) {
<div class="no-content-row">
<i class="las la-exclamation"></i>
<span>{{ 'GRANTS.EMPTY' | translate }}</span>
@@ -13,8 +13,8 @@
$list-background-color: map-get($background, 300);
$card-background-color: map-get($background, cards);
$border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
$border-selected-color: if($is-dark-theme, #fff, #000);
$border-color: if(sass($is-dark-theme): rgba(#8795a1, 0.2) ; else: rgba(#8795a1, 0.2));
$border-selected-color: if(sass($is-dark-theme): #fff; else: #000);
.home-title {
font-size: 2rem;
@@ -50,7 +50,7 @@
<div class="flow-type">
<i class="type-icon las la-dot-circle"></i>
<span>{{ flow.type?.name?.localizedMessage }}</span>
@if (flow.type && (flow.triggerActionsList?.length ?? 0) > 0) {
@if (flow.type && flow.triggerActionsList.length > 0) {
<button
matTooltip="{{ 'ACTIONS.CLEAR' | translate }}"
mat-icon-button
@@ -67,12 +67,16 @@
<mat-icon svgIcon="mdi_arrow_right_bottom" class="icon"></mat-icon>
<span>{{ trigger.triggerType?.name?.localizedMessage }}</span>
<span class="fill-space"></span>
<button color="warn" mat-icon-button (click)="removeTriggerActionsList(i)">
<button color="warn" mat-icon-button (click)="removeTriggerActionsList(flow, i)">
<i class="las la-trash"></i>
</button>
</div>
<span class="fill-space"></span>
<div class="flow-action-wrapper" cdkDropList (cdkDropListDropped)="drop(i, trigger.actionsList, $event)">
<div
class="flow-action-wrapper"
cdkDropList
(cdkDropListDropped)="drop(flow, i, trigger.actionsList, $event)"
>
@for (action of trigger.actionsList; track action) {
<div cdkDrag cdkDragLockAxis="y" cdkDragBoundary=".action-wrapper" class="flow-action">
<i class="las la-code"></i>
@@ -21,7 +21,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
standalone: false,
})
export class ActionsComponent {
protected flow!: Flow.AsObject;
protected flow?: Flow.AsObject;
protected typeControl: UntypedFormControl = new UntypedFormControl();
@@ -124,23 +124,23 @@ export class ActionsComponent {
});
}
drop(triggerActionsListIndex: number, array: any[], event: CdkDragDrop<Action.AsObject[]>) {
drop(flow: Flow.AsObject, triggerActionsListIndex: number, array: any[], event: CdkDragDrop<Action.AsObject[]>) {
moveItemInArray(array, event.previousIndex, event.currentIndex);
this.saveFlow(triggerActionsListIndex);
this.saveFlow(flow, triggerActionsListIndex);
}
saveFlow(index: number) {
saveFlow(flow: Flow.AsObject, index: number) {
if (
this.flow.type &&
this.flow.triggerActionsList &&
this.flow.triggerActionsList[index] &&
this.flow.triggerActionsList[index]?.triggerType
flow.type &&
flow.triggerActionsList &&
flow.triggerActionsList[index] &&
flow.triggerActionsList[index]?.triggerType
) {
this.mgmtService
.setTriggerActions(
this.flow.triggerActionsList[index].actionsList.map((action) => action.id),
this.flow.type.id,
this.flow.triggerActionsList[index].triggerType?.id ?? '',
flow.triggerActionsList[index].actionsList.map((action) => action.id),
flow.type.id,
flow.triggerActionsList[index].triggerType?.id ?? '',
)
.then(() => {
this.toast.showInfo('FLOWS.TOAST.ACTIONSSET', true);
@@ -151,8 +151,8 @@ export class ActionsComponent {
}
}
protected removeTriggerActionsList(index: number) {
if (this.flow.type && this.flow.triggerActionsList && this.flow.triggerActionsList[index]) {
protected removeTriggerActionsList(flow: Flow.AsObject, index: number) {
if (flow.type && flow.triggerActionsList && flow.triggerActionsList[index]) {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.CLEAR',
@@ -166,10 +166,10 @@ export class ActionsComponent {
dialogRef.afterClosed().subscribe((resp) => {
if (resp) {
this.mgmtService
.setTriggerActions([], this.flow?.type?.id ?? '', this.flow.triggerActionsList[index].triggerType?.id ?? '')
.setTriggerActions([], flow?.type?.id ?? '', flow.triggerActionsList[index].triggerType?.id ?? '')
.then(() => {
this.toast.showInfo('FLOWS.TOAST.ACTIONSSET', true);
this.loadFlow(this.flow?.type?.id ?? '');
this.loadFlow(flow?.type?.id ?? '');
})
.catch((error) => {
this.toast.showError(error);
@@ -68,12 +68,12 @@
</cnsl-info-section>
}
@if (app && app.oidcConfig && app.oidcConfig.complianceProblemsList && app.oidcConfig.complianceProblemsList?.length) {
@if (app && app.oidcConfig && app.oidcConfig.complianceProblemsList && app.oidcConfig.complianceProblemsList.length) {
<div class="compliance">
<h2 class="compliance-title">{{ 'APP.COMPLIANCE' | translate }}</h2>
<cnsl-info-section class="problem" [type]="InfoSectionType.ALERT">
<ul style="margin: 0">
@for (problem of app.oidcConfig?.complianceProblemsList || []; track problem) {
@for (problem of app.oidcConfig.complianceProblemsList; track problem) {
<li style="margin: 0 0 0.5rem 0">
{{ problem.localizedMessage }}
</li>
@@ -444,7 +444,7 @@
@if (currentSetting.id === 'additional-origins') {
<cnsl-card title=" {{ 'APP.ADDITIONALORIGINS' | translate }}">
<p class="app-desc cnsl-secondary-text">{{ 'APP.ADDITIONALORIGINSDESC' | translate }}</p>
@if (additionalOriginsList && appType && appType?.value !== undefined) {
@if (additionalOriginsList && appType?.value !== undefined) {
<cnsl-additional-origins
class="app-input"
[canWrite]="canWrite"
@@ -507,7 +507,7 @@
description="{{ 'USER.MACHINE.KEYSDESC' | translate }}"
>
<cnsl-client-keys [projectId]="projectId" [appId]="app.id"></cnsl-client-keys>
@if (apiForm && app?.apiConfig) {
@if (apiForm && app.apiConfig) {
<div class="btn-container">
<button
class="submit-button"
@@ -160,7 +160,7 @@
.current-auth-method {
display: flex;
align-items: center;
border: 1px solid if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
border: 1px solid if(sass($is-dark-theme): rgba(#8795a1, 0.2) ; else: rgba(#8795a1, 0.2));
color: white;
border-radius: 0.5rem;
padding: 0.5rem 1rem;
@@ -115,8 +115,8 @@ h1 {
$list-background-color: map-get($background, 300);
$card-background-color: map-get($background, cards);
$border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
$border-selected-color: if($is-dark-theme, #fff, #000);
$border-color: if(sass($is-dark-theme): rgba(#8795a1, 0.2) ; else: rgba(#8795a1, 0.2));
$border-selected-color: if(sass($is-dark-theme): #fff; else: #000);
.integrate-layout-container {
background: map-get($background, metadata-section);
@@ -172,11 +172,11 @@ h1 {
}
.dark-only {
display: if($is-dark-theme, block, none);
display: if(sass($is-dark-theme): block; else: none);
}
.light-only {
display: if($is-dark-theme, none, block);
display: if(sass($is-dark-theme): none; else: block);
}
span {
@@ -6,7 +6,7 @@
>
@if (currentCreateStep === 1) {
<h1>{{ 'PROJECT.GRANT.CREATE.SEL_ORG' | translate }}</h1>
<cnsl-search-org-autocomplete class="block" (selectionChanged)="selectOrg($event)"> </cnsl-search-org-autocomplete>
<cnsl-search-org-autocomplete (selectionChanged)="selectOrg($event)" class="block"> </cnsl-search-org-autocomplete>
@if (org) {
<span> {{ 'PROJECT.GRANT.CREATE.FOR_ORG' | translate }} {{ org.name }} - {{ org.primaryDomain }} </span>
}
@@ -1,11 +1,11 @@
import { Location } from '@angular/common';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Component, DestroyRef, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Subject, takeUntil } from 'rxjs';
import { Org } from 'src/app/proto/generated/zitadel/org_pb';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
import { Org } from '@zitadel/proto/zitadel/org_pb';
import { NewMgmtService } from 'src/app/services/new-mgmt.service';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
const ROUTEPARAM = 'projectid';
@@ -15,8 +15,8 @@ const ROUTEPARAM = 'projectid';
styleUrls: ['./project-grant-create.component.scss'],
standalone: false,
})
export class ProjectGrantCreateComponent implements OnInit, OnDestroy {
public org?: Org.AsObject;
export class ProjectGrantCreateComponent implements OnInit {
public org?: Org;
public projectId: string = '';
public grantId: string = '';
public rolesKeyList: string[] = [];
@@ -24,17 +24,17 @@ export class ProjectGrantCreateComponent implements OnInit, OnDestroy {
public createSteps: number = 2;
public currentCreateStep: number = 1;
private destroy$: Subject<void> = new Subject();
constructor(
private route: ActivatedRoute,
private toast: ToastService,
private mgmtService: ManagementService,
private _location: Location,
private breadcrumbService: BreadcrumbService,
private readonly route: ActivatedRoute,
private readonly toast: ToastService,
private readonly mgmtService: NewMgmtService,
private readonly _location: Location,
private readonly breadcrumbService: BreadcrumbService,
private readonly destroyRef: DestroyRef,
) {}
public ngOnInit(): void {
this.route.params.pipe(takeUntil(this.destroy$)).subscribe((params) => {
this.route.params.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((params) => {
this.projectId = params['projectid'];
const breadcrumbs = [
@@ -53,25 +53,6 @@ export class ProjectGrantCreateComponent implements OnInit, OnDestroy {
});
}
public ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
public searchOrg(domain: string): void {
this.mgmtService
.getOrgByDomainGlobal(domain)
.then((ret) => {
if (ret.org) {
const tmp = ret.org;
this.org = tmp;
}
})
.catch((error) => {
this.toast.showError(error);
});
}
public close(): void {
this._location.back();
}
@@ -79,7 +60,7 @@ export class ProjectGrantCreateComponent implements OnInit, OnDestroy {
public addGrant(): void {
if (this.org) {
this.mgmtService
.addProjectGrant(this.org.id, this.projectId, this.rolesKeyList)
.addProjectGrant({ grantedOrgId: this.org.id, projectId: this.projectId, roleKeys: this.rolesKeyList })
.then(() => {
this.close();
})
@@ -89,7 +70,7 @@ export class ProjectGrantCreateComponent implements OnInit, OnDestroy {
}
}
public selectOrg(org: Org.AsObject): void {
public selectOrg(org: Org): void {
this.org = org;
}
@@ -14,8 +14,8 @@ import { CreateLayoutModule } from 'src/app/modules/create-layout/create-layout.
import { InputModule } from 'src/app/modules/input/input.module';
import { ProjectRolesTableModule } from 'src/app/modules/project-roles-table/project-roles-table.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { SearchOrgAutocompleteComponent } from 'src/app/modules/search-org-autocomplete/search-org-autocomplete.component';
import { SearchOrgAutocompleteModule } from 'src/app/modules/search-org-autocomplete/search-org-autocomplete.module';
import { ProjectGrantCreateRoutingModule } from './project-grant-create-routing.module';
import { ProjectGrantCreateComponent } from './project-grant-create.component';
@@ -39,7 +39,7 @@ import { ProjectGrantCreateComponent } from './project-grant-create.component';
MatProgressSpinnerModule,
FormsModule,
TranslateModule,
SearchOrgAutocompleteModule,
SearchOrgAutocompleteComponent,
],
})
export default class ProjectGrantCreateModule {}
@@ -28,13 +28,13 @@
</div>
<mat-menu #actions="matMenu" xPosition="before">
@if (grant?.state === ProjectGrantState.PROJECT_GRANT_STATE_ACTIVE) {
<button mat-menu-item (click)="changeState(ProjectGrantState.PROJECT_GRANT_STATE_INACTIVE)">
@if (grant && grant.state === ProjectGrantState.PROJECT_GRANT_STATE_ACTIVE) {
<button mat-menu-item (click)="changeState(grant, ProjectGrantState.PROJECT_GRANT_STATE_INACTIVE)">
{{ 'USER.PAGES.DEACTIVATE' | translate }}
</button>
}
@if (grant?.state === ProjectGrantState.PROJECT_GRANT_STATE_INACTIVE) {
<button mat-menu-item (click)="changeState(ProjectGrantState.PROJECT_GRANT_STATE_ACTIVE)">
@if (grant && grant.state === ProjectGrantState.PROJECT_GRANT_STATE_INACTIVE) {
<button mat-menu-item (click)="changeState(grant, ProjectGrantState.PROJECT_GRANT_STATE_ACTIVE)">
{{ 'USER.PAGES.REACTIVATE' | translate }}
</button>
}
@@ -48,8 +48,8 @@
<cnsl-project-grant-illustration
[grantedProject]="grant"
[projectRoleOptions]="projectRoleOptions"
(roleRemoved)="removeRole($event)"
(editRoleClicked)="editRoles()"
(roleRemoved)="removeRole(grant, $event)"
(editRoleClicked)="editRoles(grant)"
>
</cnsl-project-grant-illustration>
}
@@ -63,15 +63,15 @@
[canWrite]="['project.grant.member.write', 'project.grant.member.write:' + grant.grantId] | hasRole | async"
[canDelete]="['project.grant.member.delete', 'project.grant.member.delete:' + grant.grantId] | hasRole | async"
[memberRoleOptions]="memberRoleOptions"
(updateRoles)="updateMemberRoles($event.member, $event.change)"
(deleteMember)="removeProjectMember($event)"
(updateRoles)="updateMemberRoles(grant, $event.member, $event.change)"
(deleteMember)="removeProjectMember(grant, $event)"
[factoryLoadFunc]="changePageFactory"
(changedSelection)="selection = $event"
[refreshTrigger]="changePage"
>
<button
selectactions
(click)="removeProjectMemberSelection()"
(click)="removeProjectMemberSelection(grant)"
[disabled]="
(['project.grant.member.delete', 'project.grant.member.delete:' + grant.grantId] | hasRole | async) === false
"
@@ -82,7 +82,7 @@
<div class="cnsl-action-button">
<i class="las la-trash"></i>
<span>{{ 'ACTIONS.SELECTIONDELETE' | translate }}</span>
<cnsl-action-keys [type]="ActionKeysType.DELETE" (actionTriggered)="removeProjectMemberSelection()">
<cnsl-action-keys [type]="ActionKeysType.DELETE" (actionTriggered)="removeProjectMemberSelection(grant)">
</cnsl-action-keys>
</div>
</button>
@@ -92,13 +92,13 @@
[disabled]="
(['project.grant.member.write', 'project.grant.member.write:' + grant.grantId] | hasRole | async) === false
"
(click)="openAddMember()"
(click)="openAddMember(grant)"
color="primary"
mat-raised-button
>
<div class="cnsl-action-button">
<mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }}
<cnsl-action-keys (actionTriggered)="openAddMember()"> </cnsl-action-keys>
<cnsl-action-keys (actionTriggered)="openAddMember(grant)"> </cnsl-action-keys>
</div>
</button>
</cnsl-members-table>
@@ -27,7 +27,7 @@ import { ProjectGrantMembersDataSource } from './project-grant-members-datasourc
export class ProjectGrantDetailComponent {
public INITIALPAGESIZE: number = 25;
public grant!: GrantedProject.AsObject;
public grant: GrantedProject.AsObject | undefined;
public projectid: string = '';
public grantid: string = '';
@@ -99,13 +99,13 @@ export class ProjectGrantDetailComponent {
});
}
public changeState(newState: ProjectGrantState): void {
public changeState(grant: GrantedProject.AsObject, newState: ProjectGrantState): void {
if (newState === ProjectGrantState.PROJECT_GRANT_STATE_ACTIVE) {
this.mgmtService
.reactivateProjectGrant(this.grantid, this.projectid)
.then(() => {
this.toast.showInfo('PROJECT.TOAST.REACTIVATED', true);
this.grant.state = newState;
grant.state = newState;
})
.catch((error) => {
this.toast.showError(error);
@@ -115,7 +115,7 @@ export class ProjectGrantDetailComponent {
.deactivateProjectGrant(this.grantid, this.projectid)
.then(() => {
this.toast.showInfo('PROJECT.TOAST.DEACTIVATED', true);
this.grant.state = newState;
grant.state = newState;
setTimeout(() => {
this.changePage.emit();
}, 1000);
@@ -144,56 +144,25 @@ export class ProjectGrantDetailComponent {
});
}
updateRoles(selectionChange: string[]): void {
this.mgmtService
.updateProjectGrant(this.grant.grantId, this.grant.projectId, selectionChange)
.then(() => {
this.toast.showInfo('PROJECT.GRANT.TOAST.PROJECTGRANTUPDATED', true);
setTimeout(() => {
this.changePage.emit();
}, 1000);
})
.catch((error) => {
this.toast.showError(error);
this.changePage.emit();
});
public async removeProjectMemberSelection(grant: GrantedProject.AsObject) {
await Promise.all(this.selection.map((member) => this.removeProjectMember(grant, member)));
}
public removeProjectMemberSelection(): void {
Promise.all(
this.selection.map((member) => {
return this.mgmtService
.removeProjectGrantMember(this.grant.projectId, this.grant.grantId, member.userId)
.then(() => {
this.toast.showInfo('PROJECT.GRANT.TOAST.PROJECTGRANTMEMBERREMOVED', true);
setTimeout(() => {
this.changePage.emit();
}, 1000);
})
.catch((error) => {
this.changePage.emit();
this.toast.showError(error);
});
}),
);
public async removeProjectMember(grant: GrantedProject.AsObject, member: Member.AsObject) {
try {
await this.mgmtService.removeProjectGrantMember(grant.projectId, grant.grantId, member.userId);
this.toast.showInfo('PROJECT.GRANT.TOAST.PROJECTGRANTMEMBERREMOVED', true);
await new Promise<void>((res) => setTimeout(res, 1000));
} catch (error) {
this.toast.showError(error);
} finally {
this.changePage.emit();
}
}
public removeProjectMember(member: Member.AsObject): void {
this.mgmtService
.removeProjectGrantMember(this.grant.projectId, this.grant.grantId, member.userId)
.then(() => {
this.toast.showInfo('PROJECT.GRANT.TOAST.PROJECTGRANTMEMBERREMOVED', true);
setTimeout(() => {
this.changePage.emit();
}, 1000);
})
.catch((error) => {
this.changePage.emit();
this.toast.showError(error);
});
}
public async openAddMember(): Promise<any> {
public async openAddMember(grant: GrantedProject.AsObject): Promise<any> {
const dialogRef = this.dialog.open(MemberCreateDialogComponent, {
data: {
creationType: CreationType.PROJECT_GRANTED,
@@ -210,7 +179,7 @@ export class ProjectGrantDetailComponent {
const userIds = users.map((user) => user.id);
Promise.all(
userIds.map((userid: string) => {
return this.mgmtService.addProjectGrantMember(this.grant.projectId, this.grant.grantId, userid, resp.roles);
return this.mgmtService.addProjectGrantMember(grant.projectId, grant.grantId, userid, resp.roles);
}),
)
.then(() => {
@@ -228,9 +197,9 @@ export class ProjectGrantDetailComponent {
});
}
updateMemberRoles(member: Member.AsObject, selectionChange: string[]): void {
updateMemberRoles(grant: GrantedProject.AsObject, member: Member.AsObject, selectionChange: string[]): void {
this.mgmtService
.updateProjectGrantMember(this.grant.projectId, this.grant.grantId, member.userId, selectionChange)
.updateProjectGrantMember(grant.projectId, grant.grantId, member.userId, selectionChange)
.then(() => {
setTimeout(() => {
this.changePage.emit();
@@ -243,13 +212,13 @@ export class ProjectGrantDetailComponent {
});
}
removeRole(role: string): void {
const index = this.grant.grantedRoleKeysList.findIndex((r) => r === role);
removeRole(grant: GrantedProject.AsObject, role: string): void {
const index = grant.grantedRoleKeysList.findIndex((r) => r === role);
if (index > -1) {
this.grant.grantedRoleKeysList.splice(index, 1);
grant.grantedRoleKeysList.splice(index, 1);
this.mgmtService
.updateProjectGrant(this.grant.grantId, this.grant.projectId, this.grant.grantedRoleKeysList)
.updateProjectGrant(grant.grantId, grant.projectId, grant.grantedRoleKeysList)
.then(() => {
setTimeout(() => {
this.changePage.emit();
@@ -263,13 +232,13 @@ export class ProjectGrantDetailComponent {
}
}
public editRoles(): void {
public editRoles(grant: GrantedProject.AsObject): void {
const dialogRef = this.dialog.open<UserGrantRoleDialogComponent, UserGrantRoleDialogData, UserGrantRoleDialogResult>(
UserGrantRoleDialogComponent,
{
data: {
projectId: this.grant.projectId,
selectedRoleKeysList: this.grant.grantedRoleKeysList,
projectId: grant.projectId,
selectedRoleKeysList: grant.grantedRoleKeysList,
i18nTitle: 'PROJECT.GRANT.EDITTITLE',
},
width: '600px',
@@ -281,10 +250,10 @@ export class ProjectGrantDetailComponent {
return;
}
this.mgmtService
.updateProjectGrant(this.grant.grantId, this.grant.projectId, resp.roles)
.updateProjectGrant(grant.grantId, grant.projectId, resp.roles)
.then(() => {
this.toast.showInfo('PROJECT.GRANT.TOAST.PROJECTGRANTUPDATED', true);
this.grant.grantedRoleKeysList = resp.roles;
grant.grantedRoleKeysList = resp.roles;
setTimeout(() => {
this.changePage.emit();
}, 1000);
@@ -1,9 +1,9 @@
@mixin project-grant-illustration-theme($theme) {
$foreground: map-get($theme, foreground);
$is-dark-theme: map-get($theme, is-dark);
$overlayColor: if($is-dark-theme, #ffffff10, #00000008);
$highlightedOverlayColor: if($is-dark-theme, #68c4cf14, #68c4cf14);
$borderColor: if($is-dark-theme, #ffffff20, #00000020);
$overlayColor: if(sass($is-dark-theme): #ffffff10; else: #00000008);
$highlightedOverlayColor: if(sass($is-dark-theme): #68c4cf14; else: #68c4cf14);
$borderColor: if(sass($is-dark-theme): #ffffff20; else: #00000020);
.pgi {
display: flex;
@@ -136,7 +136,7 @@
<tr class="highlight" mat-row *matRowDef="let grant; columns: displayedColumns"></tr>
</table>
</div>
@if ((dataSource.loading$ | async) === false && !dataSource?.totalResult) {
@if ((dataSource.loading$ | async) === false && !dataSource.totalResult) {
<div class="no-content-row">
<i class="las la-exclamation"></i>
<span>{{ 'PROJECT.GRANT.EMPTY' | translate }}</span>
@@ -158,7 +158,7 @@
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
</div>
@if ((loading$ | async) === false && !dataSource?.data?.length) {
@if ((loading$ | async) === false && !dataSource.data.length) {
<div class="no-content-row">
<i class="las la-exclamation"></i>
<span>{{ 'PROJECT.TABLE.EMPTY' | translate }}</span>
@@ -73,7 +73,7 @@
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
@if ((loading$ | async) === false && !dataSource?.data?.length) {
@if ((loading$ | async) === false && !dataSource.data.length) {
<div class="no-content-row">
<i class="las la-exclamation"></i>
<span>{{ 'USER.PASSWORDLESS.EMPTY' | translate }}</span>
@@ -93,7 +93,7 @@
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
@if ((loading$ | async) === false && !dataSource?.data?.length) {
@if ((loading$ | async) === false && !dataSource.data.length) {
<div class="no-content-row">
<i class="las la-exclamation"></i>
<span>{{ 'USER.MFA.EMPTY' | translate }}</span>
@@ -74,7 +74,7 @@
</table>
</div>
@if ((loading$ | async) === false && !dataSource?.data?.length) {
@if ((loading$ | async) === false && !dataSource.data.length) {
<div class="no-content-row">
<i class="las la-exclamation"></i>
<span>{{ 'USER.EXTERNALIDP.EMPTY' | translate }}</span>
@@ -72,7 +72,7 @@
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
@if ((loading$ | async) === false && !dataSource?.data?.length) {
@if ((loading$ | async) === false && !dataSource.data.length) {
<div class="no-content-row">
<i class="las la-exclamation"></i>
<span>{{ 'USER.PASSWORDLESS.EMPTY' | translate }}</span>
@@ -233,7 +233,7 @@
></tr>
</table>
</div>
@if (!loading() && !dataSource?.data?.length) {
@if (!loading() && !dataSource.data.length) {
<div class="no-content-row">
<i class="las la-exclamation"></i>
<span>{{ 'USER.TABLE.EMPTY' | translate }}</span>
+6 -1
View File
@@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { GrpcService } from './grpc.service';
import { create } from '@bufbuild/protobuf';
import { create, MessageInitShape } from '@bufbuild/protobuf';
import {
AddMyAuthFactorOTPSMSResponse,
GetMyLoginPolicyResponse,
@@ -19,6 +19,7 @@ import {
ListMyMetadataResponse,
VerifyMyPhoneResponse,
ListMyZitadelPermissionsResponse,
ListMyProjectOrgsRequestSchema,
} from '@zitadel/proto/zitadel/auth_pb';
import { injectQuery } from '@tanstack/angular-query-experimental';
import { UserService } from './user.service';
@@ -82,4 +83,8 @@ export class NewAuthService {
queryFn: () => this.listMyZitadelPermissions().then(({ result }) => result),
}));
}
public listMyProjectOrgs(req: MessageInitShape<typeof ListMyProjectOrgsRequestSchema>, signal?: AbortSignal) {
return this.grpcService.authNew.listMyProjectOrgs(req, { signal });
}
}
@@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
import { GrpcService } from './grpc.service';
import {
AddOrgResponse,
AddProjectGrantRequestSchema,
DeactivateOrgResponse,
GenerateMachineSecretRequestSchema,
GenerateMachineSecretResponse,
@@ -123,4 +124,8 @@ export class NewMgmtService {
public addOrg(name: string): Promise<AddOrgResponse> {
return this.grpcService.mgmtNew.addOrg({ name });
}
public addProjectGrant(req: MessageInitShape<typeof AddProjectGrantRequestSchema>) {
return this.grpcService.mgmtNew.addProjectGrant(req);
}
}
+2 -2
View File
@@ -4,11 +4,11 @@
$is-dark-theme: map.get($theme, is-dark);
.cm-s-material.CodeMirror {
background-color: if($is-dark-theme, #00000020, #263238);
background-color: if(sass($is-dark-theme): #00000020; else: #263238);
color: #eff;
font-size: 14px;
border-radius: 4px;
border: 1px solid if($is-dark-theme, #403e3e, #00000040);
border: 1px solid if(sass($is-dark-theme): #403e3e; else: #00000040);
}
}
+6 -6
View File
@@ -20,8 +20,8 @@
transform: all 0.2 linear;
font-size: 1rem;
border: none;
border: 1px solid if($is-dark-theme, #f9f7f775, #1a191954);
background-color: if($is-dark-theme, #00000040, #00000004);
border: 1px solid if(sass($is-dark-theme): #f9f7f775; else: #1a191954);
background-color: if(sass($is-dark-theme): #00000040; else: #00000004);
border-radius: 4px;
height: 40px;
padding: 10px;
@@ -34,7 +34,7 @@
margin-bottom: 2px;
&:hover {
border-color: if($is-dark-theme, #e0e0e0, #1a1b1b);
border-color: if(sass($is-dark-theme): #e0e0e0; else: #1a1b1b);
}
&:active,
@@ -49,11 +49,11 @@
}
&[disabled] {
border-color: if($is-dark-theme, #36373850, #cccdce50);
color: if($is-dark-theme, #ffffff80, #00000061);
border-color: if(sass($is-dark-theme): #36373850; else: #cccdce50);
color: if(sass($is-dark-theme): #ffffff80; else: #00000061);
&::placeholder {
color: if($is-dark-theme, #ffffff80, #00000061);
color: if(sass($is-dark-theme): #ffffff80; else: #00000061);
}
}
}
+14 -14
View File
@@ -88,7 +88,7 @@
}
.no-content-row {
background-color: if($is-dark-theme, #ffffff10, #f4f4f470);
background-color: if(sass($is-dark-theme): #ffffff10; else: #f4f4f470);
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
display: flex;
@@ -123,41 +123,41 @@
font-weight: 500;
font-size: 12px;
white-space: nowrap;
background-color: if($is-dark-theme, map.get($background, state), #e4e7e4);
background-color: if(sass($is-dark-theme): map.get($background, state) ; else: #e4e7e4);
box-shadow: 0 0 3px #0000001a;
&.active {
background-color: if($is-dark-theme, map.get($background, state-active), #cbf4c9);
color: if($is-dark-theme, #cbf4c9, #0e6245);
background-color: if(sass($is-dark-theme): map.get($background, state-active) ; else: #cbf4c9);
color: if(sass($is-dark-theme): #cbf4c9; else: #0e6245);
}
&.inactive {
color: if($is-dark-theme, #ffc1c1, #620e0e);
background-color: if($is-dark-theme, map.get($background, state-inactive), #ffc1c1);
color: if(sass($is-dark-theme): #ffc1c1; else: #620e0e);
background-color: if(sass($is-dark-theme): map.get($background, state-inactive) ; else: #ffc1c1);
}
&.neutral {
background: if($is-dark-theme, #01489c78, #47a8ff82);
color: if($is-dark-theme, #47a8ff, #01489c);
background: if(sass($is-dark-theme): #01489c78; else: #47a8ff82);
color: if(sass($is-dark-theme): #47a8ff; else: #01489c);
}
}
.bg-state {
background-color: if($is-dark-theme, map.get($background, state), #e4e7e4) !important;
background-color: if(sass($is-dark-theme): map.get($background, state) ; else: #e4e7e4) !important;
&.active {
background-color: if($is-dark-theme, map.get($background, state-active), #cbf4c9) !important;
color: if($is-dark-theme, #cbf4c9, #0e6245) !important;
background-color: if(sass($is-dark-theme): map.get($background, state-active) ; else: #cbf4c9) !important;
color: if(sass($is-dark-theme): #cbf4c9; else: #0e6245) !important;
}
&.inactive {
color: if($is-dark-theme, #ffc1c1, #620e0e) !important;
background-color: if($is-dark-theme, map.get($background, state-inactive), #ffc1c1) !important;
color: if(sass($is-dark-theme): #ffc1c1; else: #620e0e) !important;
background-color: if(sass($is-dark-theme): map.get($background, state-inactive) ; else: #ffc1c1) !important;
}
}
.color-shadow {
border: 1px solid if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
border: 1px solid if(sass($is-dark-theme): rgba(#8795a1, 0.2) ; else: rgba(#8795a1, 0.2));
box-shadow: 0 0 3px #0000001a;
}
}
+5
View File
@@ -0,0 +1,5 @@
/* https://github.com/tailwindlabs/tailwindcss-intellisense/issues/1158#issuecomment-2968949608 */
@import 'tailwindcss/utilities' prefix(tw);
@import 'tailwindcss/theme' prefix(tw);
@custom-variant dark (&:where(.dark-theme, .dark-theme *));
+28 -19
View File
@@ -432,14 +432,14 @@ importers:
specifier: ^8.5.8
version: 8.5.8
prettier:
specifier: ^3.8.1
version: 3.8.1
specifier: ^3.8.3
version: 3.8.3
prettier-plugin-organize-imports:
specifier: ^4.3.0
version: 4.3.0([email protected].1)([email protected])
version: 4.3.0([email protected].3)([email protected])
prettier-plugin-tailwindcss:
specifier: ^0.7.2
version: 0.7.2([email protected]([email protected].1)([email protected]))([email protected].1)
version: 0.7.2([email protected]([email protected].3)([email protected]))([email protected].3)
sass:
specifier: ^1.98.0
version: 1.98.0
@@ -624,6 +624,9 @@ importers:
'@eslint/js':
specifier: ^10.0.1
version: 10.0.1([email protected]([email protected]))
'@tailwindcss/postcss':
specifier: ^4.2.2
version: 4.2.2
'@types/file-saver':
specifier: ^2.0.7
version: 2.0.7
@@ -699,12 +702,18 @@ importers:
karma-jasmine-html-reporter:
specifier: ^2.2.0
version: 2.2.0([email protected])([email protected]([email protected]))([email protected])
postcss:
specifier: ^8.5.8
version: 8.5.8
prettier:
specifier: ^3.8.1
version: 3.8.1
specifier: ^3.8.3
version: 3.8.3
prettier-plugin-organize-imports:
specifier: ^4.3.0
version: 4.3.0([email protected].1)([email protected])
version: 4.3.0([email protected].3)([email protected])
tailwindcss:
specifier: ^4.2.2
version: 4.2.2
typescript:
specifier: 5.9.2
version: 5.9.2
@@ -807,8 +816,8 @@ importers:
specifier: ^9.0.3
version: 9.0.3
prettier:
specifier: ^3.8.1
version: 3.8.1
specifier: ^3.8.3
version: 3.8.3
typescript:
specifier: ^5.9.3
version: 5.9.3
@@ -10450,8 +10459,8 @@ packages:
engines: {node: '>=10.13.0'}
hasBin: true
[email protected].1:
resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==}
[email protected].3:
resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==}
engines: {node: '>=14'}
hasBin: true
@@ -23386,25 +23395,25 @@ snapshots:
[email protected]: {}
[email protected]([email protected].1)([email protected]):
[email protected]([email protected].3)([email protected]):
dependencies:
prettier: 3.8.1
prettier: 3.8.3
typescript: 5.9.2
[email protected]([email protected].1)([email protected]):
[email protected]([email protected].3)([email protected]):
dependencies:
prettier: 3.8.1
prettier: 3.8.3
typescript: 5.9.3
[email protected]([email protected]([email protected].1)([email protected]))([email protected].1):
[email protected]([email protected]([email protected].3)([email protected]))([email protected].3):
dependencies:
prettier: 3.8.1
prettier: 3.8.3
optionalDependencies:
prettier-plugin-organize-imports: 4.3.0([email protected].1)([email protected])
prettier-plugin-organize-imports: 4.3.0([email protected].3)([email protected])
[email protected]: {}
[email protected].1: {}
[email protected].3: {}
[email protected]: {}
+1 -1
View File
@@ -12,7 +12,7 @@
"cypress": "^15.13.0",
"cypress-wait-until": "^3.0.2",
"jsonwebtoken": "^9.0.3",
"prettier": "^3.8.1",
"prettier": "^3.8.3",
"typescript": "^5.9.3",
"uuid": "^13.0.0"
}