28 changed files with 1301 additions and 2 deletions
@ -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)); |
|||
} |
|||
} |
|||
@ -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(); |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -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> |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
@ -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> |
|||
|
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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' + |
|||
'```'; |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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%); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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); |
|||
}); |
|||
} |
|||
|
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
Loading…
Reference in new issue