Browse Source

UI: Added feature API keys management.

pull/14074/head
deaflynx 10 months ago
parent
commit
737ee2cd02
  1. 54
      ui-ngx/src/app/core/http/api-key.service.ts
  2. 207
      ui-ngx/src/app/modules/home/components/api-key/api-keys-table-config.ts
  3. 20
      ui-ngx/src/app/modules/home/components/api-key/api-keys-table.component.html
  4. 45
      ui-ngx/src/app/modules/home/components/api-key/api-keys-table.component.scss
  5. 80
      ui-ngx/src/app/modules/home/components/api-key/api-keys-table.component.ts
  6. 84
      ui-ngx/src/app/modules/home/components/api-key/components/dialog/add-api-key-dialog.component.html
  7. 39
      ui-ngx/src/app/modules/home/components/api-key/components/dialog/add-api-key-dialog.component.scss
  8. 111
      ui-ngx/src/app/modules/home/components/api-key/components/dialog/add-api-key-dialog.component.ts
  9. 46
      ui-ngx/src/app/modules/home/components/api-key/components/dialog/api-key-generated-dialog.component.html
  10. 85
      ui-ngx/src/app/modules/home/components/api-key/components/dialog/api-key-generated-dialog.component.scss
  11. 55
      ui-ngx/src/app/modules/home/components/api-key/components/dialog/api-key-generated-dialog.component.ts
  12. 35
      ui-ngx/src/app/modules/home/components/api-key/components/dialog/api-keys-table-dialog.component.html
  13. 36
      ui-ngx/src/app/modules/home/components/api-key/components/dialog/api-keys-table-dialog.component.scss
  14. 47
      ui-ngx/src/app/modules/home/components/api-key/components/dialog/api-keys-table-dialog.component.ts
  15. 41
      ui-ngx/src/app/modules/home/components/api-key/components/dialog/edit-api-key-description-panel.component.html
  16. 41
      ui-ngx/src/app/modules/home/components/api-key/components/dialog/edit-api-key-description-panel.component.scss
  17. 61
      ui-ngx/src/app/modules/home/components/api-key/components/dialog/edit-api-key-description-panel.component.ts
  18. 21
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  19. 14
      ui-ngx/src/app/modules/home/pages/security/security.component.html
  20. 14
      ui-ngx/src/app/modules/home/pages/security/security.component.ts
  21. 4
      ui-ngx/src/app/modules/home/pages/user/user-tabs.component.html
  22. 34
      ui-ngx/src/app/shared/models/api-key.models.ts
  23. 1
      ui-ngx/src/app/shared/models/constants.ts
  24. 23
      ui-ngx/src/app/shared/models/entity-type.models.ts
  25. 26
      ui-ngx/src/app/shared/models/id/api-key-id.ts
  26. 42
      ui-ngx/src/app/shared/pipe/date-expiration.pipe.ts
  27. 3
      ui-ngx/src/app/shared/shared.module.ts
  28. 34
      ui-ngx/src/assets/locale/locale.constant-en_US.json

54
ui-ngx/src/app/core/http/api-key.service.ts

@ -0,0 +1,54 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// 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.
///
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
import { Observable } from 'rxjs';
import { PageLink } from '@shared/models/page/page-link';
import { PageData } from '@shared/models/page/page-data';
import { ApiKeyInfo, ApiKey } from '@shared/models/api-key.models';
@Injectable({
providedIn: 'root'
})
export class ApiKeyService {
constructor(
private http: HttpClient
) {
}
public saveApiKey(apiKey: ApiKeyInfo, config?: RequestConfig): Observable<ApiKey> {
return this.http.post<ApiKey>('/api/apiKey', apiKey, defaultHttpOptionsFromConfig(config));
}
public deleteApiKey(id: string, config?: RequestConfig): Observable<void> {
return this.http.delete<void>(`/api/apiKey/${id}`, defaultHttpOptionsFromConfig(config));
}
public updateApiKeyDescription(id: string, description: string, config?: RequestConfig): Observable<ApiKeyInfo> {
return this.http.put<ApiKeyInfo>(`/api/apiKey/${id}/description`, description, defaultHttpOptionsFromConfig(config));
}
public enableApiKey(id: string, enabledValue: boolean, config?: RequestConfig): Observable<ApiKeyInfo> {
return this.http.put<ApiKeyInfo>(`/api/apiKey/${id}/enabled/${enabledValue}`, defaultHttpOptionsFromConfig(config));
}
public getUserApiKeys(userId: string, pageLink: PageLink, config?: RequestConfig): Observable<PageData<ApiKeyInfo>> {
return this.http.get<PageData<ApiKeyInfo>>(`/api/apiKeys/${userId}${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config));
}
}

207
ui-ngx/src/app/modules/home/components/api-key/api-keys-table-config.ts

@ -0,0 +1,207 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// 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.
///
import {
DateEntityTableColumn,
EntityTableColumn,
EntityTableConfig
} from '@home/models/entity/entities-table-config.models';
import { EntityType, EntityTypeResource, entityTypeTranslations } from '@shared/models/entity-type.models';
import { Direction } from '@shared/models/page/sort-order';
import { TranslateService } from '@ngx-translate/core';
import { MatDialog } from '@angular/material/dialog';
import { Injectable, Renderer2, ViewContainerRef } from '@angular/core';
import { DatePipe } from '@angular/common';
import { Observable } from 'rxjs';
import { ApiKeyInfo, ApiKey } from '@shared/models/api-key.models';
import { ApiKeyService } from '@core/http/api-key.service';
import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe';
import { TbPopoverService } from '@shared/components/popover.service';
import { map } from 'rxjs/operators';
import { UserId } from '@shared/models/id/user-id';
import { AddApiKeyDialogComponent } from '@home/components/api-key/components/dialog/add-api-key-dialog.component';
import {
EditApiKeyDescriptionPanelComponent
} from '@home/components/api-key/components/dialog/edit-api-key-description-panel.component';
import { ApiKeysTableDialogData } from '@home/components/api-key/components/dialog/api-keys-table-dialog.component';
import {
ApiKeyGeneratedDialogComponent, ApiKeyGeneratedDialogData
} from '@home/components/api-key/components/dialog/api-key-generated-dialog.component';
@Injectable()
export class ApiKeysTableConfig extends EntityTableConfig<ApiKeyInfo> {
constructor(
private apiKeyService: ApiKeyService,
private translate: TranslateService,
private customTranslate: CustomTranslatePipe,
private dialog: MatDialog,
private datePipe: DatePipe,
private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef,
private userId: UserId,
) {
super();
this.entityType = EntityType.API_KEY;
this.detailsPanelEnabled = false;
this.addAsTextButton = true;
this.pageMode = false;
this.entityTranslations = entityTypeTranslations.get(EntityType.API_KEY);
this.entityResources = {} as EntityTypeResource<ApiKeyInfo>;
this.tableTitle = this.translate.instant('api-key.api-keys');
this.entitiesFetchFunction = pageLink => this.apiKeyService.getUserApiKeys(this.userId.id, pageLink);
this.addEntity = () => this.addApiKey();
this.deleteEntityTitle = entity => this.translate.instant('api-key.delete-api-key-title', {name: entity.description});
this.deleteEntityContent = () => this.translate.instant('api-key.delete-api-key-text');
this.deleteEntitiesTitle = count => this.translate.instant('api-key.delete-api-keys-title', {count});
this.deleteEntitiesContent = () => this.translate.instant('api-key.delete-api-keys-text');
this.deleteEntity = id => this.apiKeyService.deleteApiKey(id.id);
this.cellActionDescriptors = [{
name: '',
nameFunction: (entity) =>
this.translate.instant(entity.enabled ? 'api-key.disable' : 'api-key.enable'),
icon: 'mdi:toggle-switch',
isEnabled: (entity) => !entity.expired,
iconFunction: (entity) => entity.enabled ? 'mdi:toggle-switch' : 'mdi:toggle-switch-off-outline',
onAction: ($event, entity) => this.toggleEnableMode($event, entity)
}];
this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC};
this.columns.push(
new DateEntityTableColumn<ApiKeyInfo>('createdTime', 'common.created-time', this.datePipe, '170px'),
new EntityTableColumn<ApiKeyInfo>('description', 'api-key.description', '100%',
(entity) => this.customTranslate.transform(entity?.description), () => ({}), true, () => ({}),
(entity) => entity?.description.length > 80 ? this.customTranslate.transform(entity.description) : undefined, false,
{
name: this.translate.instant('api-key.edit-description'),
icon: 'edit',
isEnabled: () => true,
onAction: ($event, entity) => this.updateApiKeyDescription($event, entity)
}),
new EntityTableColumn<ApiKeyInfo>('active', 'api-key.status', '80px',
entity => this.apiKeyStatus(entity), entity => this.apiKeyStatusStyle(entity), false),
new EntityTableColumn<ApiKeyInfo>('expirationTime', 'api-key.expiration-time', '120px',
(entity) => entity.expirationTime != 0 ?
this.datePipe.transform(entity.expirationTime, 'dd/MM/yyyy, HH:mm') :
this.translate.instant('api-key.expiration-time-never'),
),
);
}
private addApiKey(): Observable<ApiKey> {
return this.dialog.open<AddApiKeyDialogComponent, ApiKeysTableDialogData, ApiKey>(AddApiKeyDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
userId: this.userId
}
}).afterClosed().pipe(map(res => {
if (res) {
this.apiKeyGenerated(res);
} else {
return null;
}
}));
}
private apiKeyGenerated(apiKey: ApiKey) {
this.dialog.open<ApiKeyGeneratedDialogComponent, ApiKeyGeneratedDialogData>(ApiKeyGeneratedDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
apiKey
}
}).afterClosed()
.subscribe(() => {
this.updateData();
});
}
private toggleEnableMode($event: Event, entity: ApiKeyInfo): void {
if ($event) {
$event.stopPropagation();
}
this.apiKeyService.enableApiKey(entity.id.id, !entity.enabled, {ignoreLoading: true})
.subscribe(
() => this.updateData()
);
}
private apiKeyStatus(apiKey: ApiKeyInfo): string {
let translateKey = 'api-key.status-active';
let backgroundColor = 'rgba(25, 128, 56, 0.08)';
if (apiKey.expired) {
translateKey = 'api-key.status-expired';
backgroundColor = 'rgba(0, 0, 0, 0.04)';
} else if (!apiKey.enabled) {
translateKey = 'api-key.status-inactive';
backgroundColor = 'rgba(209, 39, 48, 0.08)';
}
return `<div class="status" style="border-radius: 16px; height: 32px;
line-height: 32px; padding: 0 12px; width: fit-content; background-color: ${backgroundColor}">
${this.translate.instant(translateKey)}
</div>`;
}
private apiKeyStatusStyle(apiKey: ApiKeyInfo): object {
const styleObj = {
fontSize: '14px',
color: '#198038',
cursor: 'pointer'
};
if (apiKey.expired) {
styleObj.color = 'rgba(0, 0, 0, 0.54)';
} else if (!apiKey.enabled) {
styleObj.color = '#d12730';
}
return styleObj;
}
private updateApiKeyDescription($event: Event, entity: ApiKeyInfo) {
if ($event) {
$event.stopPropagation();
}
const trigger = ($event.target || $event.srcElement || $event.currentTarget) as Element;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const editSecretDescriptionPanelPopover = this.popoverService.displayPopover({
trigger,
renderer: this.renderer,
componentType: EditApiKeyDescriptionPanelComponent,
hostView: this.viewContainerRef,
preferredPlacement: ['right', 'bottom', 'top'],
context: {
apiKeyId: entity.id.id,
description: entity.description
},
isModal: true
});
editSecretDescriptionPanelPopover.tbComponentRef.instance.descriptionApplied.subscribe(() => {
editSecretDescriptionPanelPopover.hide();
this.updateData();
});
}
}
}

20
ui-ngx/src/app/modules/home/components/api-key/api-keys-table.component.html

@ -0,0 +1,20 @@
<!--
Copyright © 2016-2025 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
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.
-->
@if (apiKeysTableConfig) {
<tb-entities-table [entitiesTableConfig]="apiKeysTableConfig" [class.tb-details-mode]="true"></tb-entities-table>
}

45
ui-ngx/src/app/modules/home/components/api-key/api-keys-table.component.scss

@ -0,0 +1,45 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
@import '../../src/scss/constants.scss';
@import '../../src/scss/mixins';
:host ::ng-deep {
tb-entities-table {
.mat-drawer-container {
background-color: white;
mat-cell.mat-column-description {
white-space: nowrap;
.mat-mdc-icon-button {
vertical-align: middle;
margin-left: 8px;
@include tb-mat-icon-button-size(32);
.mat-icon {
@include tb-mat-icon-size(20);
}
}
span {
display: inline-block;
max-width: 40ch;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
pointer-events: none;
}
}
}
}
}

80
ui-ngx/src/app/modules/home/components/api-key/api-keys-table.component.ts

@ -0,0 +1,80 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// 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.
///
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
effect,
input,
Renderer2,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { EntitiesTableComponent } from '@home/components/entity/entities-table.component';
import { TranslateService } from '@ngx-translate/core';
import { MatDialog } from '@angular/material/dialog';
import { DatePipe } from '@angular/common';
import { ApiKeysTableConfig } from '@home/components/api-key/api-keys-table-config';
import { ApiKeyService } from '@core/http/api-key.service';
import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe';
import { TbPopoverService } from '@shared/components/popover.service';
import { UserId } from '@shared/models/id/user-id';
@Component({
selector: 'tb-api-keys-table',
templateUrl: './api-keys-table.component.html',
styleUrls: ['./api-keys-table.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ApiKeysTableComponent {
@ViewChild(EntitiesTableComponent, {static: true}) entitiesTable: EntitiesTableComponent;
active = input<boolean>();
userId = input<UserId>();
apiKeysTableConfig: ApiKeysTableConfig;
constructor(
private apiKeyService: ApiKeyService,
private translate: TranslateService,
private customTranslate: CustomTranslatePipe,
private dialog: MatDialog,
private datePipe: DatePipe,
private cd: ChangeDetectorRef,
private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef,
) {
effect(() => {
if (this.active()) {
this.apiKeysTableConfig = new ApiKeysTableConfig(
this.apiKeyService,
this.translate,
this.customTranslate,
this.dialog,
this.datePipe,
this.popoverService,
this.renderer,
this.viewContainerRef,
this.userId(),
);
this.cd.markForCheck();
}
});
}
}

84
ui-ngx/src/app/modules/home/components/api-key/components/dialog/add-api-key-dialog.component.html

@ -0,0 +1,84 @@
<!--
Copyright © 2016-2025 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
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.
-->
<form (ngSubmit)="add()">
<mat-toolbar color="primary">
<h2>{{ 'api-key.generate-title' | translate }}</h2>
<span class="flex-1"></span>
<div tb-help="apiKeys"></div>
<button mat-icon-button
(click)="close()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
@if (isLoading$ | async) {
<mat-progress-bar color="warn" mode="indeterminate"></mat-progress-bar>
} @else {
<div style="height: 4px;"></div>
}
<div mat-dialog-content>
<section class="tb-form-panel no-border no-padding" [formGroup]="apiKeyForm">
<div class="api-key-text">
<span translate>api-key.generate-text</span>
</div>
<mat-form-field class="mat-block" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>api-key.description</mat-label>
<textarea #input cdkTextareaAutosize matInput formControlName="description" rows="2" maxLength="255"></textarea>
</mat-form-field>
<mat-slide-toggle class="mat-slide" formControlName="enabled">
{{ 'api-key.enable' | translate }}
</mat-slide-toggle>
<section class="flex gap-3">
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="flex-1">
<mat-select formControlName="expirationTime" aria-label="Expiration date selector" (selectionChange)="onExpirationDateChange()">
<mat-option [value]="value" *ngFor="let value of expirationDates">
{{ value | dateExpiration }}
</mat-option>
<mat-option value="never">{{'api-key.expiration-time-never' | translate}}</mat-option>
<mat-option value="custom">{{'api-key.expiration-time-custom' | translate}}</mat-option>
</mat-select>
</mat-form-field>
@if (isCustomExpirationTime()) {
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>api-key.date</mat-label>
<mat-datetimepicker-toggle [for]="datePicker" matSuffix></mat-datetimepicker-toggle>
<mat-datetimepicker #datePicker type="datetime" openOnFocus="true"></mat-datetimepicker>
<input matInput required formControlName="customExpirationTime"
[matDatetimepicker]="datePicker"
[min]="startDate"/>
</mat-form-field>
}
</section>
</section>
</div>
<div mat-dialog-actions class="flex items-center justify-end">
<button mat-button color="primary"
type="button"
cdkFocusInitial
[disabled]="(isLoading$ | async)"
(click)="close()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || apiKeyForm?.invalid">
{{ 'api-key.generate' | translate }}
</button>
</div>
</form>

39
ui-ngx/src/app/modules/home/components/api-key/components/dialog/add-api-key-dialog.component.scss

@ -0,0 +1,39 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
@import '../../src/scss/constants';
:host{
form {
width: 700px;
}
.api-key-text {
position: relative;
padding: 8px 16px 8px 16px;
&::before {
content: '';
position: absolute;
inset: 0;
background-color: $tb-primary-color;
border-radius: 6px;
opacity: 0.04;
}
span {
font-size: 12px;
color: rgba(0, 0, 0, 0.54);
}
}
}

111
ui-ngx/src/app/modules/home/components/api-key/components/dialog/add-api-key-dialog.component.ts

@ -0,0 +1,111 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// 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.
///
import { Component, OnInit, Inject } from '@angular/core';
import { DialogComponent } from '@shared/components/dialog.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { FormBuilder, Validators, UntypedFormGroup } from '@angular/forms';
import { deepTrim } from '@core/utils';
import { ApiKeyService } from '@core/http/api-key.service';
import { ApiKeyInfo } from '@shared/models/api-key.models';
import { ApiKeysTableDialogData } from '@home/components/api-key/components/dialog/api-keys-table-dialog.component';
import { DAY } from '@shared/models/time/time.models';
@Component({
selector: 'tb-add-api-key-dialog',
templateUrl: './add-api-key-dialog.component.html',
styleUrls: ['./add-api-key-dialog.component.scss']
})
export class AddApiKeyDialogComponent extends DialogComponent<AddApiKeyDialogComponent, ApiKeyInfo | string> implements OnInit{
apiKeyForm: UntypedFormGroup;
readonly startDate = new Date();
readonly expirationDates = [7, 30, 60, 90].map(days => days * DAY);
private defaultExpirationDate = this.expirationDates[1];
constructor(
protected store: Store<AppState>,
protected router: Router,
public dialogRef: MatDialogRef<AddApiKeyDialogComponent, ApiKeyInfo | string>,
private fb: FormBuilder,
private apiKeyService: ApiKeyService,
@Inject(MAT_DIALOG_DATA) public data: ApiKeysTableDialogData,
) {
super(store, router, dialogRef);
}
ngOnInit() {
this.apiKeyForm = this.fb.group({
description: [{value: null, disabled: false}, [Validators.required]],
enabled: [{value: true, disabled: false}, []],
expirationTime: [{value: this.defaultExpirationDate, disabled: false}, [Validators.required]],
customExpirationTime: [{value: null, disabled: true}, []],
});
}
close(): void {
this.dialogRef.close(null);
}
add(): void {
const formValue = this.apiKeyForm.value;
const userId = this.data.userId;
const expirationTime = this.calcExpirationTime();
const apiKey = {
...deepTrim(formValue),
expirationTime,
userId,
} as ApiKeyInfo;
this.apiKeyService.saveApiKey(apiKey).subscribe(
(res) => {
this.dialogRef.close(res);
}
);
}
isCustomExpirationTime() {
return this.apiKeyForm.value?.expirationTime === 'custom';
}
onExpirationDateChange() {
const customExpirationTimeControl = this.apiKeyForm.get('customExpirationTime');
if (this.isCustomExpirationTime()) {
customExpirationTimeControl.enable();
} else {
customExpirationTimeControl.disable();
}
customExpirationTimeControl.updateValueAndValidity();
}
private calcExpirationTime(): number {
const expirationTimeValue = this.apiKeyForm.get('expirationTime').value;
let value: number;
if (this.isCustomExpirationTime()) {
value = this.apiKeyForm.get('customExpirationTime').value.getTime();
} else if (expirationTimeValue === 'never') {
value = 0;
} else {
value = expirationTimeValue + Date.now();
}
return value;
}
}

46
ui-ngx/src/app/modules/home/components/api-key/components/dialog/api-key-generated-dialog.component.html

@ -0,0 +1,46 @@
<!--
Copyright © 2016-2025 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
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.
-->
<mat-toolbar color="primary">
<h2>{{ 'api-key.generated-title' | translate }}</h2>
<span class="flex-1"></span>
<button mat-icon-button
(click)="close()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content>
<span translate>api-key.generated-text</span>
<div class="tb-form-panel no-padding no-border tb-tab-body">
<tb-markdown usePlainMarkdown containerClass="tb-command-code"
[data]='createMarkDownCommand(data.apiKey.value)'></tb-markdown>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>api-key.generated-command-title</div>
<tb-markdown usePlainMarkdown containerClass="tb-command-code"
[data]='createMarkDownCommand(apiKeyCommand)'></tb-markdown>
</div>
</div>
</div>
<div mat-dialog-actions class="justify-end">
<button mat-raised-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="close()">
{{ 'action.close' | translate }}
</button>
</div>

85
ui-ngx/src/app/modules/home/components/api-key/components/dialog/api-key-generated-dialog.component.scss

@ -0,0 +1,85 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
:host{
display: block;
width: 600px;
max-width: 100%;
color: rgba(0, 0, 0, 0.76);
}
:host ::ng-deep {
.tb-markdown-view {
.tb-command-code {
.code-wrapper {
padding: 0;
pre[class*=language-] {
margin: 0;
background: #F3F6FA;
border-color: #305680;
padding-right: 38px;
padding-bottom: 4px;
min-height: 42px;
scrollbar-width: thin;
&::-webkit-scrollbar {
width: 4px;
height: 4px;
}
}
}
button.clipboard-btn {
right: -2px;
p {
color: #305680;
}
p, div {
background-color: #F3F6FA;
}
div {
img {
display: none;
}
&:after {
content: "";
position: initial;
display: block;
width: 18px;
height: 18px;
background: #305680;
mask-image: url(/assets/copy-code-icon.svg);
-webkit-mask-image: url(/assets/copy-code-icon.svg);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
}
}
}
}
}
.mdc-button__label > span {
.mat-icon {
vertical-align: text-bottom;
box-sizing: initial;
}
}
.tabs-icon {
margin-right: 8px;
}
.tb-form-panel.tb-tab-body {
padding: 16px 0 0;
}
}

55
ui-ngx/src/app/modules/home/components/api-key/components/dialog/api-key-generated-dialog.component.ts

@ -0,0 +1,55 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// 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.
///
import { Component, Inject } from '@angular/core';
import { DialogComponent } from '@shared/components/dialog.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { userInfoCommand, ApiKey } from '@shared/models/api-key.models';
export interface ApiKeyGeneratedDialogData {
apiKey: ApiKey;
}
@Component({
selector: 'tb-api-key-generated-dialog',
templateUrl: './api-key-generated-dialog.component.html',
styleUrls: ['api-key-generated-dialog.component.scss']
})
export class ApiKeyGeneratedDialogComponent extends DialogComponent<ApiKeyGeneratedDialogComponent, void> {
apiKeyCommand = userInfoCommand(this.data.apiKey.value);
constructor(protected store: Store<AppState>,
protected router: Router,
protected dialogRef: MatDialogRef<ApiKeyGeneratedDialogComponent, void>,
@Inject(MAT_DIALOG_DATA) public data: ApiKeyGeneratedDialogData) {
super(store, router, dialogRef);
}
close(): void {
this.dialogRef.close(null);
}
createMarkDownCommand(command: string): string {
return '```bash\n' +
command +
'{:copy-code}\n' +
'```';
}
}

35
ui-ngx/src/app/modules/home/components/api-key/components/dialog/api-keys-table-dialog.component.html

@ -0,0 +1,35 @@
<!--
Copyright © 2016-2025 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
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.
-->
<mat-toolbar color="primary">
<h2>{{ 'api-key.manage-api-keys' | translate }}</h2>
<span class="flex-1"></span>
<div tb-help="apiKeys"></div>
<button mat-icon-button
(click)="close()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<tb-api-keys-table [active]="true" [userId]="data.userId"/>
<div mat-dialog-actions class="justify-end">
<button mat-raised-button color="primary"
type="button"
(click)="close()">
{{ 'action.close' | translate }}
</button>
</div>

36
ui-ngx/src/app/modules/home/components/api-key/components/dialog/api-keys-table-dialog.component.scss

@ -0,0 +1,36 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
:host ::ng-deep {
tb-api-keys-table {
tb-entities-table {
.tb-absolute-fill {
position: relative;
}
.table-container {
width: 1000px;
min-height: 625px;
max-height: 625px;
}
.no-data-found {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
}
}

47
ui-ngx/src/app/modules/home/components/api-key/components/dialog/api-keys-table-dialog.component.ts

@ -0,0 +1,47 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// 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.
///
import { Component, Inject } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { UserId } from '@shared/models/id/user-id';
export interface ApiKeysTableDialogData {
userId: UserId;
}
@Component({
selector: 'tb-api-keys-table-dialog',
templateUrl: './api-keys-table-dialog.component.html',
styleUrls: ['api-keys-table-dialog.component.scss']
})
export class ApiKeysTableDialogComponent {
constructor(
protected store: Store<AppState>,
protected router: Router,
public dialogRef: MatDialogRef<ApiKeysTableDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: ApiKeysTableDialogData,
) {
}
close(): void {
this.dialogRef.close(null);
}
}

41
ui-ngx/src/app/modules/home/components/api-key/components/dialog/edit-api-key-description-panel.component.html

@ -0,0 +1,41 @@
<!--
Copyright © 2016-2025 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
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.
-->
<div class="tb-edit-api-key-description-panel">
<div class="tb-edit-api-key-description-title">{{ 'api-key.edit-description' | translate }}</div>
<mat-form-field class="mat-block" appearance="outline">
<mat-label translate>api-key.description</mat-label>
<textarea #input cdkTextareaAutosize matInput [formControl]="descriptionFormControl" rows="2" maxLength="255"></textarea>
<mat-hint align="end">{{input.value?.length || 0}}/255</mat-hint>
</mat-form-field>
<div class="tb-edit-api-key-description-panel-buttons">
<button mat-button
color="primary"
type="button"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button
color="primary"
type="button"
(click)="applyDescription()"
[disabled]="descriptionFormControl.invalid || !descriptionFormControl.dirty">
{{ 'action.save' | translate }}
</button>
</div>
</div>

41
ui-ngx/src/app/modules/home/components/api-key/components/dialog/edit-api-key-description-panel.component.scss

@ -0,0 +1,41 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
.tb-edit-api-key-description-panel {
--mdc-outlined-text-field-outline-color: rgba(0,0,0,0.12);
width: 400px;
max-width: 90vw;
display: flex;
flex-direction: column;
gap: 16px;
.tb-edit-api-key-description-title {
font-size: 16px;
font-weight: 500;
line-height: 24px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87);
}
.tb-edit-api-key-description-panel-buttons {
height: 40px;
display: flex;
flex-direction: row;
gap: 16px;
justify-content: flex-end;
align-items: flex-end;
}
}

61
ui-ngx/src/app/modules/home/components/api-key/components/dialog/edit-api-key-description-panel.component.ts

@ -0,0 +1,61 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// 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.
///
import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { ApiKeyService } from '@core/http/api-key.service';
@Component({
selector: 'tb-edit-api-key-description-panel',
templateUrl: './edit-api-key-description-panel.component.html',
styleUrls: ['./edit-api-key-description-panel.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class EditApiKeyDescriptionPanelComponent implements OnInit {
@Input()
apiKeyId: string;
@Input()
description: string;
@Output()
descriptionApplied = new EventEmitter<string>();
descriptionFormControl = this.fb.control<string>(null);
constructor(private fb: FormBuilder,
private popover: TbPopoverComponent<EditApiKeyDescriptionPanelComponent>,
private apiKeyService: ApiKeyService) {}
ngOnInit(): void {
this.descriptionFormControl.setValue(this.description, {emitEvent: false});
}
cancel() {
this.popover.hide();
}
applyDescription() {
const description = this.descriptionFormControl.value.trim();
this.apiKeyService.updateApiKeyDescription(this.apiKeyId, description).subscribe(() => {
this.descriptionApplied.emit(description);
});
}
}

21
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -197,6 +197,17 @@ import {
import { CalculatedFieldsModule } from '@home/components/calculated-fields/calculated-field.module';
import { AlarmRuleModule } from "@home/components/alarm-rules/alarm-rule.module";
import { AlarmRulesTableComponent } from "@home/components/alarm-rules/alarm-rules-table.component";
import { ApiKeysTableComponent } from '@home/components/api-key/api-keys-table.component';
import { AddApiKeyDialogComponent } from '@home/components/api-key/components/dialog/add-api-key-dialog.component';
import {
EditApiKeyDescriptionPanelComponent
} from '@home/components/api-key/components/dialog/edit-api-key-description-panel.component';
import {
ApiKeyGeneratedDialogComponent
} from '@home/components/api-key/components/dialog/api-key-generated-dialog.component';
import {
ApiKeysTableDialogComponent
} from '@home/components/api-key/components/dialog/api-keys-table-dialog.component';
@NgModule({
declarations:
@ -348,6 +359,11 @@ import { AlarmRulesTableComponent } from "@home/components/alarm-rules/alarm-rul
AIModelDialogComponent,
ResourcesDialogComponent,
ResourcesLibraryComponent,
ApiKeysTableComponent,
ApiKeysTableDialogComponent,
AddApiKeyDialogComponent,
EditApiKeyDescriptionPanelComponent,
ApiKeyGeneratedDialogComponent,
],
imports: [
CommonModule,
@ -494,6 +510,11 @@ import { AlarmRulesTableComponent } from "@home/components/alarm-rules/alarm-rul
AIModelDialogComponent,
ResourcesDialogComponent,
ResourcesLibraryComponent,
ApiKeysTableComponent,
ApiKeysTableDialogComponent,
AddApiKeyDialogComponent,
EditApiKeyDescriptionPanelComponent,
ApiKeyGeneratedDialogComponent,
],
providers: [
WidgetComponentService,

14
ui-ngx/src/app/modules/home/pages/security/security.component.html

@ -30,6 +30,20 @@
</button>
</div>
</mat-card>
<mat-card appearance="outlined" class="profile-card flex flex-col">
<div class="flex flex-row items-center justify-between">
<mat-card-title>
<span class="mat-headline-5 card-title" translate>api-key.api-keys</span>
</mat-card-title>
<button mat-raised-button
color="primary"
type="button"
(click)="openApiKeysTable()">
<mat-icon>key</mat-icon>
<span>{{ 'api-key.manage' | translate }}</span>
</button>
</div>
</mat-card>
<mat-card appearance="outlined" class="profile-card flex flex-col">
<div class="change-password" tb-toast toastTarget="changePassword">
<form #changePasswordForm="ngForm" [formGroup]="changePassword" (ngSubmit)="onChangePassword(changePasswordForm)">

14
ui-ngx/src/app/modules/home/pages/security/security.component.ts

@ -52,6 +52,9 @@ import { isDefinedAndNotNull, isEqual } from '@core/utils';
import { AuthService } from '@core/auth/auth.service';
import { UserPasswordPolicy } from '@shared/models/settings.models';
import { MatCheckboxChange } from '@angular/material/checkbox';
import {
ApiKeysTableDialogComponent, ApiKeysTableDialogData
} from '@home/components/api-key/components/dialog/api-keys-table-dialog.component';
@Component({
selector: 'tb-security',
@ -384,4 +387,15 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro
newPassword2: ''
});
}
openApiKeysTable() {
this.dialog.open<ApiKeysTableDialogComponent, ApiKeysTableDialogData>(
ApiKeysTableDialogComponent, {
disableClose: false,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
userId: this.user.id,
}
}).afterClosed().subscribe();
}
}

4
ui-ngx/src/app/modules/home/pages/user/user-tabs.component.html

@ -40,3 +40,7 @@
label="{{ 'audit-log.audit-logs' | translate }}" #auditLogsTab="matTab">
<tb-audit-log-table [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.USER" [userId]="entity.id" detailsMode="true"></tb-audit-log-table>
</mat-tab>
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN"
label="{{ 'api-key.api-keys' | translate }}" #apiKeysTab="matTab">
<tb-api-keys-table [active]="apiKeysTab.isActive" [userId]="entity.id"></tb-api-keys-table>
</mat-tab>

34
ui-ngx/src/app/shared/models/api-key.models.ts

@ -0,0 +1,34 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// 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.
///
import { BaseData } from '@shared/models/base-data';
import { HasTenantId } from '@shared/models/entity.models';
import { ApiKeyId } from '@shared/models/id/api-key-id';
import { UserId } from '@shared/models/id/user-id';
export const userInfoCommand = (key: string): string => `curl -X GET "${window.location.origin}/api/auth/user" -H "Content-Type: application/json" -H "X-Authorization: ApiKey ${key}"`
export interface ApiKeyInfo extends BaseData<ApiKeyId>, HasTenantId {
enabled: boolean;
expirationTime: number;
description: string;
expired: boolean;
userId: UserId;
}
export interface ApiKey extends ApiKeyInfo {
value: string;
}

1
ui-ngx/src/app/shared/models/constants.ts

@ -215,6 +215,7 @@ export const HelpLinks = {
mobileQrCode: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/mobile-qr-code/`,
calculatedField: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/calculated-fields/`,
aiModels: `${helpBaseUrl}/docs${docPlatformPrefix}/samples/analytics/ai-models/`,
apiKeys: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/api-keys`,
timewindowSettings: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/dashboards/#time-window`,
trendzSettings: `${helpBaseUrl}/docs/trendz/`
}

23
ui-ngx/src/app/shared/models/entity-type.models.ts

@ -52,6 +52,7 @@ export enum EntityType {
MOBILE_APP = 'MOBILE_APP',
CALCULATED_FIELD = 'CALCULATED_FIELD',
AI_MODEL = 'AI_MODEL',
API_KEY = 'API_KEY',
}
export enum AliasEntityType {
@ -506,7 +507,19 @@ export const entityTypeTranslations = new Map<EntityType | AliasEntityType, Enti
search: 'action.search',
selectedEntities: 'ai-models.selected-fields'
}
]
],
[
EntityType.API_KEY,
{
type: 'entity.type-api-key',
typePlural: 'entity.type-api-keys',
list: 'api-key.list',
add: 'api-key.generate',
noEntities: 'api-key.no-found',
search: 'api-key.search',
selectedEntities: 'api-key.selected-api-keys'
}
],
]
);
@ -644,7 +657,13 @@ export const entityTypeResources = new Map<EntityType, EntityTypeResource<BaseDa
{
helpLinkId: 'aiModels'
}
]
],
[
EntityType.API_KEY,
{
helpLinkId: 'apiKeys'
}
],
]
);

26
ui-ngx/src/app/shared/models/id/api-key-id.ts

@ -0,0 +1,26 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// 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.
///
import { EntityId } from './entity-id';
import { EntityType } from '@shared/models/entity-type.models';
export class ApiKeyId implements EntityId {
entityType = EntityType.API_KEY;
id: string;
constructor(id: string) {
this.id = id;
}
}

42
ui-ngx/src/app/shared/pipe/date-expiration.pipe.ts

@ -0,0 +1,42 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// 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.
///
import { Pipe, PipeTransform } from '@angular/core';
import { DatePipe } from '@angular/common';
import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe';
import { isDefined } from '@core/utils';
@Pipe({
name: 'dateExpiration'
})
export class DateExpirationPipe implements PipeTransform {
constructor(private millisecondsToTimeString: MillisecondsToTimeStringPipe, private datePipe: DatePipe) {
}
transform(expirationMs: number, arg?: any): string {
const displayDate = isDefined(arg?.displayDate) ? arg.displayDate : true;
const dateFormat = isDefined(arg?.dateFormat) ? arg.dateFormat : ' (dd/MM/yyyy)';
const shortFormat = isDefined(arg?.shortFormat) ? arg.shortFormat : true;
const onlyFirstDigit = isDefined(arg?.onlyFirstDigit) ? arg.onlyFirstDigit : true;
let time = this.millisecondsToTimeString.transform(expirationMs, shortFormat, onlyFirstDigit);
if (displayDate) {
const exactDate = this.datePipe.transform(expirationMs + Date.now(), dateFormat);
time += exactDate;
}
return time;
}
}

3
ui-ngx/src/app/shared/shared.module.ts

@ -229,6 +229,7 @@ import { EntityKeyAutocompleteComponent } from '@shared/components/entity/entity
import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe';
import { MqttVersionSelectComponent } from '@shared/components/mqtt-version-select.component';
import { TimeUnitInputComponent } from '@shared/components/time-unit-input.component';
import { DateExpirationPipe } from '@shared/pipe/date-expiration.pipe';
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
return markedOptionsService;
@ -394,6 +395,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
ShortNumberPipe,
SelectableColumnsPipe,
KeyboardShortcutPipe,
DateExpirationPipe,
TbJsonToStringDirective,
JsonObjectEditDialogComponent,
HistorySelectorComponent,
@ -660,6 +662,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
SafePipe,
ShortNumberPipe,
SelectableColumnsPipe,
DateExpirationPipe,
RouterModule,
TranslateModule,
JsonObjectEditDialogComponent,

34
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -980,6 +980,38 @@
"edge-uplink-messages": "Edge uplink messages",
"edge-uplink-messages-per-edge": "Edge uplink messages per edge"
},
"api-key": {
"api-key": "API key",
"api-keys": "API keys",
"delete-api-key-title": "Are you sure you want to delete the API key '{{name}}'?",
"delete-api-key-text": "Be careful, after the confirmation key will become unrecoverable.",
"delete-api-keys-title": "Are you sure you want to delete { count, plural, =1 {1 API key} other {# API keys} }?",
"delete-api-keys-text": "Be careful, after the confirmation all selected keys will become unrecoverable.",
"date": "Date",
"description": "Description",
"disable": "Disable",
"edit-description": "Edit description",
"enable": "Enable API key ",
"expiration-time": "Expiration time",
"expiration-time-never": "Never",
"expiration-time-custom": "Custom",
"generate": "Generate",
"generate-title": "Generate API key",
"generate-text": "Note: The API key's permissions will inherit the rights of the user account that generates it.",
"generated-title": "API key successfully generated",
"generated-text": "Make sure to copy and save your API key now as you will not be able to see it again.",
"generated-command-title": "Execute following command to display information about current user:",
"list": "{ count, plural, =1 {One API key} other {List of # API keys} }",
"manage": "Manage",
"manage-api-keys": "Manage API keys",
"no-found": "No API keys found",
"selected-api-keys": "{ count, plural, =1 {1 API key} other {# API keys} } selected",
"search": "Search API keys",
"status": "Status",
"status-active": "Active",
"status-inactive": "Inactive",
"status-expired": "Expired"
},
"audit-log": {
"audit": "Audit",
"audit-logs": "Audit logs",
@ -2942,6 +2974,8 @@
"type-rulenodes": "Rule nodes",
"list-of-rulenodes": "{ count, plural, =1 {One rule node} other {List of # rule nodes} }",
"rulenode-name-starts-with": "Rule nodes whose names start with '{{prefix}}'",
"type-api-key": "API key",
"type-api-keys": "API keys",
"type-current-customer": "Current Customer",
"type-current-tenant": "Current Tenant",
"type-current-user": "Current User",

Loading…
Cancel
Save