feat(cnsl): docs link can be customized and custom button is available (#7840)

* feat: customize doc link and additional custom link

* feat: add e2e tests

* fix: update docs

* fix: add @peintnermax changes about cache

* fix: golangci-lint complains preparation.PrepareCommands

---------

Co-authored-by: Max Peintner <[email protected]>
This commit is contained in:
Miguel Cabrerizo
2024-05-13 16:01:50 +02:00
committed by GitHub
co-authored by Max Peintner
parent 6942324741
commit 15d5338b91
61 changed files with 1000 additions and 286 deletions
@@ -1,11 +1,11 @@
<div class="footer-wrapper">
<div class="footer-row">
<div class="footer-links">
<a target="_blank" *ngIf="policy?.tosLink" rel="noreferrer" [href]="policy?.tosLink" external>
<div class="footer-links" *ngIf="authService.privacypolicy | async as pP">
<a target="_blank" *ngIf="pP?.tosLink" rel="noreferrer" [href]="pP?.tosLink" external>
<span>{{ 'FOOTER.LINKS.TOS' | translate }}</span>
<i class="las la-external-link-alt"></i>
</a>
<a target="_blank" *ngIf="policy?.privacyLink" rel="noreferrer" [href]="policy?.privacyLink" external>
<a target="_blank" *ngIf="pP?.privacyLink" rel="noreferrer" [href]="pP?.privacyLink" external>
<span>{{ 'FOOTER.LINKS.PP' | translate }}</span>
<i class="las la-external-link-alt"></i>
</a>
@@ -8,16 +8,7 @@ import { faXTwitter } from '@fortawesome/free-brands-svg-icons';
templateUrl: './footer.component.html',
styleUrls: ['./footer.component.scss'],
})
export class FooterComponent implements OnInit {
public policy?: PrivacyPolicy.AsObject;
export class FooterComponent {
public faXTwitter = faXTwitter;
constructor(public authService: GrpcAuthService) {}
ngOnInit(): void {
this.authService.getMyPrivacyPolicy().then((policyResp) => {
if (policyResp.policy) {
this.policy = policyResp.policy;
}
});
}
}
@@ -168,7 +168,11 @@
<span class="fill-space"></span>
<a class="doc-link" href="https://zitadel.com/docs" mat-stroked-button target="_blank">
<a class="custom-link" *ngIf="customLink && customLinkText" href="{{ customLink }}" mat-stroked-button target="_blank">
{{ customLinkText }}
</a>
<a class="doc-link" *ngIf="docsLink" href="{{ docsLink }}" mat-stroked-button target="_blank">
{{ 'MENU.DOCUMENTATION' | translate }}
</a>
@@ -224,7 +224,8 @@
flex: 1;
}
.doc-link {
.doc-link,
.custom-link {
margin-right: 1rem;
@media only screen and (max-width: 800px) {
@@ -1,5 +1,5 @@
import { ConnectedPosition, ConnectionPositionPair } from '@angular/cdk/overlay';
import { Component, ElementRef, EventEmitter, Input, OnDestroy, Output, ViewChild } from '@angular/core';
import { Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { BehaviorSubject, Observable, of, Subject } from 'rxjs';
import { Org } from 'src/app/proto/generated/zitadel/org_pb';
@@ -8,8 +8,8 @@ import { AuthenticationService } from 'src/app/services/authentication.service';
import { BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ActionKeysType } from '../action-keys/action-keys.component';
import { GetPrivacyPolicyResponse } from 'src/app/proto/generated/zitadel/management_pb';
@Component({
selector: 'cnsl-header',
@@ -31,6 +31,9 @@ export class HeaderComponent implements OnDestroy {
private destroy$: Subject<void> = new Subject();
public BreadcrumbType: any = BreadcrumbType;
public ActionKeysType: any = ActionKeysType;
public docsLink = 'https://zitadel.com/docs';
public customLink = '';
public customLinkText = '';
public positions: ConnectedPosition[] = [
new ConnectionPositionPair({ originX: 'start', originY: 'bottom' }, { overlayX: 'start', overlayY: 'top' }, 0, 10),
@@ -47,7 +50,25 @@ export class HeaderComponent implements OnDestroy {
public mgmtService: ManagementService,
public breadcrumbService: BreadcrumbService,
public router: Router,
) {}
) {
this.loadData();
}
public async loadData(): Promise<any> {
const getData = (): Promise<GetPrivacyPolicyResponse.AsObject> => {
return this.mgmtService.getPrivacyPolicy();
};
getData()
.then((resp) => {
if (resp.policy) {
this.docsLink = resp.policy.docsLink;
this.customLink = resp.policy.customLink;
this.customLinkText = resp.policy.customLinkText;
}
})
.catch(() => {});
}
public ngOnDestroy() {
this.destroy$.next();
@@ -9,6 +9,7 @@
color="warn"
(click)="resetDefault()"
mat-stroked-button
data-e2e="reset-button"
>
{{ 'POLICY.RESET' | translate }}
</button>
@@ -40,7 +41,23 @@
<cnsl-form-field class="privacy-policy-formfield">
<cnsl-label>{{ 'POLICY.PRIVACY_POLICY.SUPPORTEMAIL' | translate }}</cnsl-label>
<input cnslInput name="supportEmail" formControlName="supportEmail" />
<template [ngTemplateOutlet]="templateRef" [ngTemplateOutletContext]="{ key: 'supportEmail' }"></template>
</cnsl-form-field>
<cnsl-form-field class="privacy-policy-formfield">
<cnsl-label>{{ 'POLICY.PRIVACY_POLICY.CUSTOMLINK' | translate }}</cnsl-label>
<input cnslInput name="customLink" formControlName="customLink" />
<template [ngTemplateOutlet]="templateRef" [ngTemplateOutletContext]="{ key: 'customLink' }"></template>
</cnsl-form-field>
<cnsl-form-field class="privacy-policy-formfield">
<cnsl-label>{{ 'POLICY.PRIVACY_POLICY.CUSTOMLINKTEXT' | translate }}</cnsl-label>
<input cnslInput name="customLinkText" formControlName="customLinkText" />
</cnsl-form-field>
<cnsl-form-field class="privacy-policy-formfield">
<cnsl-label>{{ 'POLICY.PRIVACY_POLICY.DOCSLINK' | translate }}</cnsl-label>
<input cnslInput name="docsLink" formControlName="docsLink" />
<template [ngTemplateOutlet]="templateRef" [ngTemplateOutletContext]="{ key: 'docsLink' }"></template>
</cnsl-form-field>
</form>
</div>
@@ -53,6 +70,7 @@
color="primary"
type="submit"
mat-raised-button
data-e2e="save-button"
>
{{ 'ACTIONS.SAVE' | translate }}
</button>
@@ -61,6 +61,9 @@ export class PrivacyPolicyComponent implements OnInit, OnDestroy {
privacyLink: ['', []],
helpLink: ['', []],
supportEmail: ['', []],
docsLink: ['', []],
customLink: ['', []],
customLinkText: ['', []],
});
this.canWrite$.pipe(take(1)).subscribe((canWrite) => {
@@ -107,6 +110,9 @@ export class PrivacyPolicyComponent implements OnInit, OnDestroy {
privacyLink: '',
helpLink: '',
supportEmail: '',
docsLink: '',
customLink: '',
customLinkText: '',
});
}
})
@@ -117,6 +123,9 @@ export class PrivacyPolicyComponent implements OnInit, OnDestroy {
privacyLink: '',
helpLink: '',
supportEmail: '',
docsLink: '',
customLink: '',
customLinkText: '',
});
});
}
@@ -129,11 +138,16 @@ export class PrivacyPolicyComponent implements OnInit, OnDestroy {
req.setTosLink(this.form.get('tosLink')?.value);
req.setHelpLink(this.form.get('helpLink')?.value);
req.setSupportEmail(this.form.get('supportEmail')?.value);
req.setDocsLink(this.form.get('docsLink')?.value);
req.setCustomLink(this.form.get('customLink')?.value);
req.setCustomLinkText(this.form.get('customLinkText')?.value);
(this.service as ManagementService)
.addCustomPrivacyPolicy(req)
.then(() => {
this.toast.showInfo('POLICY.PRIVACY_POLICY.SAVED', true);
this.loadData();
// Reload console as links may have changed
this.reloadConsole();
})
.catch((error) => this.toast.showError(error));
} else {
@@ -142,12 +156,17 @@ export class PrivacyPolicyComponent implements OnInit, OnDestroy {
req.setTosLink(this.form.get('tosLink')?.value);
req.setHelpLink(this.form.get('helpLink')?.value);
req.setSupportEmail(this.form.get('supportEmail')?.value);
req.setDocsLink(this.form.get('docsLink')?.value);
req.setCustomLink(this.form.get('customLink')?.value);
req.setCustomLinkText(this.form.get('customLinkText')?.value);
(this.service as ManagementService)
.updateCustomPrivacyPolicy(req)
.then(() => {
this.toast.showInfo('POLICY.PRIVACY_POLICY.SAVED', true);
this.loadData();
// Reload console as links may have changed
this.reloadConsole();
})
.catch((error) => this.toast.showError(error));
}
@@ -157,12 +176,17 @@ export class PrivacyPolicyComponent implements OnInit, OnDestroy {
req.setTosLink(this.form.get('tosLink')?.value);
req.setHelpLink(this.form.get('helpLink')?.value);
req.setSupportEmail(this.form.get('supportEmail')?.value);
req.setDocsLink(this.form.get('docsLink')?.value);
req.setCustomLink(this.form.get('customLink')?.value);
req.setCustomLinkText(this.form.get('customLinkText')?.value);
(this.service as AdminService)
.updatePrivacyPolicy(req)
.then(() => {
this.toast.showInfo('POLICY.PRIVACY_POLICY.SAVED', true);
this.loadData();
// Reload console as links may have changed
this.reloadConsole();
})
.catch((error) => this.toast.showError(error));
}
@@ -188,6 +212,7 @@ export class PrivacyPolicyComponent implements OnInit, OnDestroy {
.then(() => {
setTimeout(() => {
this.loadData();
window.location.reload();
}, 1000);
})
.catch((error) => {
@@ -209,4 +234,10 @@ export class PrivacyPolicyComponent implements OnInit, OnDestroy {
return false;
}
}
private reloadConsole(): void {
setTimeout(() => {
window.location.reload();
}, 1000);
}
}
@@ -36,7 +36,7 @@ export const APPEARANCE_GROUP: SettingLinks = {
};
export const PRIVACY_POLICY: SettingLinks = {
i18nTitle: 'SETTINGS.LIST.PRIVACYPOLICY',
i18nTitle: 'DESCRIPTIONS.SETTINGS.PRIVACY_POLICY.TITLE',
i18nDesc: 'POLICY.PRIVACY_POLICY.DESCRIPTION',
iamRouterLink: ['/settings'],
orgRouterLink: ['/org-settings'],
@@ -187,7 +187,7 @@ export const LOGINTEXTS: SidenavSetting = {
export const PRIVACYPOLICY: SidenavSetting = {
id: 'privacypolicy',
i18nKey: 'SETTINGS.LIST.PRIVACYPOLICY',
i18nKey: 'DESCRIPTIONS.SETTINGS.PRIVACY_POLICY.TITLE',
groupI18nKey: 'SETTINGS.GROUPS.OTHER',
requiredRoles: {
[PolicyComponentServiceType.MGMT]: ['policy.read'],