mirror of https://github.com/ghostfolio/ghostfolio
3 changed files with 206 additions and 0 deletions
@ -0,0 +1,84 @@ |
|||||
|
<div class="overflow-x-auto"> |
||||
|
<table class="gf-table w-100" mat-table [dataSource]="dataSource"> |
||||
|
<ng-container matColumnDef="alias"> |
||||
|
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Alias</th> |
||||
|
<td *matCellDef="let element" class="px-1" mat-cell> |
||||
|
{{ element.alias }} |
||||
|
</td> |
||||
|
</ng-container> |
||||
|
|
||||
|
<ng-container matColumnDef="grantee"> |
||||
|
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Grantee</th> |
||||
|
<td *matCellDef="let element" class="px-1" mat-cell> |
||||
|
{{ element.grantee }} |
||||
|
</td> |
||||
|
</ng-container> |
||||
|
|
||||
|
<ng-container matColumnDef="type"> |
||||
|
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Permission</th> |
||||
|
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell> |
||||
|
<div class="align-items-center d-flex"> |
||||
|
@if (element.permissions.includes('READ')) { |
||||
|
<ion-icon class="mr-1" name="lock-open-outline" /> |
||||
|
<ng-container i18n>View</ng-container> |
||||
|
} @else if (element.permissions.includes('READ_RESTRICTED')) { |
||||
|
<ion-icon class="mr-1" name="lock-closed-outline" /> |
||||
|
<ng-container i18n>Restricted view</ng-container> |
||||
|
} |
||||
|
</div> |
||||
|
</td> |
||||
|
</ng-container> |
||||
|
|
||||
|
<ng-container matColumnDef="details"> |
||||
|
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Details</th> |
||||
|
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell> |
||||
|
@if (element.type === 'PUBLIC') { |
||||
|
<div class="align-items-center d-flex"> |
||||
|
<ion-icon class="mr-1" name="link-outline" /> |
||||
|
<a target="_blank" [href]="getPublicUrl(element.id)">{{ |
||||
|
getPublicUrl(element.id) |
||||
|
}}</a> |
||||
|
</div> |
||||
|
@if (user?.settings?.isExperimentalFeatures) { |
||||
|
<div> |
||||
|
<code |
||||
|
>GET {{ baseUrl }}/api/v1/public/{{ |
||||
|
element.id |
||||
|
}}/portfolio</code |
||||
|
> |
||||
|
</div> |
||||
|
} |
||||
|
} |
||||
|
</td> |
||||
|
</ng-container> |
||||
|
|
||||
|
<ng-container matColumnDef="actions" stickyEnd> |
||||
|
<th *matHeaderCellDef class="px-1 text-center" mat-header-cell></th> |
||||
|
|
||||
|
<td *matCellDef="let element" class="px-1 text-center" mat-cell> |
||||
|
<button |
||||
|
class="mx-1 no-min-width px-2" |
||||
|
mat-button |
||||
|
[matMenuTriggerFor]="transactionMenu" |
||||
|
(click)="$event.stopPropagation()" |
||||
|
> |
||||
|
<ion-icon name="ellipsis-horizontal" /> |
||||
|
</button> |
||||
|
<mat-menu #transactionMenu="matMenu" xPosition="before"> |
||||
|
@if (element.type === 'PUBLIC') { |
||||
|
<button mat-menu-item (click)="onCopyUrlToClipboard(element.id)"> |
||||
|
<ng-container i18n>Copy link to clipboard</ng-container> |
||||
|
</button> |
||||
|
<hr class="my-0" /> |
||||
|
} |
||||
|
<button mat-menu-item (click)="onDeleteAccess(element.id)"> |
||||
|
<ng-container i18n>Revoke</ng-container> |
||||
|
</button> |
||||
|
</mat-menu> |
||||
|
</td> |
||||
|
</ng-container> |
||||
|
|
||||
|
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr> |
||||
|
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr> |
||||
|
</table> |
||||
|
</div> |
@ -0,0 +1,11 @@ |
|||||
|
:host { |
||||
|
display: block; |
||||
|
|
||||
|
a { |
||||
|
color: rgba(var(--palette-primary-500), 1); |
||||
|
|
||||
|
&:hover { |
||||
|
color: rgba(var(--palette-primary-300), 1); |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,111 @@ |
|||||
|
import { ConfirmationDialogType } from '@ghostfolio/client/core/notification/confirmation-dialog/confirmation-dialog.type'; |
||||
|
import { NotificationService } from '@ghostfolio/client/core/notification/notification.service'; |
||||
|
import { Access, User } from '@ghostfolio/common/interfaces'; |
||||
|
import { publicRoutes } from '@ghostfolio/common/routes/routes'; |
||||
|
|
||||
|
import { Clipboard, ClipboardModule } from '@angular/cdk/clipboard'; |
||||
|
import { CommonModule } from '@angular/common'; |
||||
|
import { |
||||
|
ChangeDetectionStrategy, |
||||
|
Component, |
||||
|
CUSTOM_ELEMENTS_SCHEMA, |
||||
|
EventEmitter, |
||||
|
Input, |
||||
|
OnChanges, |
||||
|
Output |
||||
|
} from '@angular/core'; |
||||
|
import { MatButtonModule } from '@angular/material/button'; |
||||
|
import { MatMenuModule } from '@angular/material/menu'; |
||||
|
import { MatSnackBar } from '@angular/material/snack-bar'; |
||||
|
import { MatTableDataSource, MatTableModule } from '@angular/material/table'; |
||||
|
import { RouterModule } from '@angular/router'; |
||||
|
import { IonIcon } from '@ionic/angular/standalone'; |
||||
|
import { addIcons } from 'ionicons'; |
||||
|
import { |
||||
|
ellipsisHorizontal, |
||||
|
linkOutline, |
||||
|
lockClosedOutline, |
||||
|
lockOpenOutline |
||||
|
} from 'ionicons/icons'; |
||||
|
import ms from 'ms'; |
||||
|
|
||||
|
@Component({ |
||||
|
changeDetection: ChangeDetectionStrategy.OnPush, |
||||
|
imports: [ |
||||
|
ClipboardModule, |
||||
|
CommonModule, |
||||
|
IonIcon, |
||||
|
MatButtonModule, |
||||
|
MatMenuModule, |
||||
|
MatTableModule, |
||||
|
RouterModule |
||||
|
], |
||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA], |
||||
|
selector: 'gf-access-table', |
||||
|
templateUrl: './access-table.component.html', |
||||
|
styleUrls: ['./access-table.component.scss'] |
||||
|
}) |
||||
|
export class GfAccessTableComponent implements OnChanges { |
||||
|
@Input() accesses: Access[]; |
||||
|
@Input() showActions: boolean; |
||||
|
@Input() user: User; |
||||
|
|
||||
|
@Output() accessDeleted = new EventEmitter<string>(); |
||||
|
|
||||
|
public baseUrl = window.location.origin; |
||||
|
public dataSource: MatTableDataSource<Access>; |
||||
|
public displayedColumns = []; |
||||
|
|
||||
|
public constructor( |
||||
|
private clipboard: Clipboard, |
||||
|
private notificationService: NotificationService, |
||||
|
private snackBar: MatSnackBar |
||||
|
) { |
||||
|
addIcons({ |
||||
|
ellipsisHorizontal, |
||||
|
linkOutline, |
||||
|
lockClosedOutline, |
||||
|
lockOpenOutline |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public ngOnChanges() { |
||||
|
this.displayedColumns = ['alias', 'grantee', 'type', 'details']; |
||||
|
|
||||
|
if (this.showActions) { |
||||
|
this.displayedColumns.push('actions'); |
||||
|
} |
||||
|
|
||||
|
if (this.accesses) { |
||||
|
this.dataSource = new MatTableDataSource(this.accesses); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public getPublicUrl(aId: string): string { |
||||
|
const languageCode = this.user.settings.language; |
||||
|
|
||||
|
return `${this.baseUrl}/${languageCode}/${publicRoutes.public.path}/${aId}`; |
||||
|
} |
||||
|
|
||||
|
public onCopyUrlToClipboard(aId: string): void { |
||||
|
this.clipboard.copy(this.getPublicUrl(aId)); |
||||
|
|
||||
|
this.snackBar.open( |
||||
|
'✅ ' + $localize`Link has been copied to the clipboard`, |
||||
|
undefined, |
||||
|
{ |
||||
|
duration: ms('3 seconds') |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
public onDeleteAccess(aId: string) { |
||||
|
this.notificationService.confirm({ |
||||
|
confirmFn: () => { |
||||
|
this.accessDeleted.emit(aId); |
||||
|
}, |
||||
|
confirmType: ConfirmationDialogType.Warn, |
||||
|
title: $localize`Do you really want to revoke this granted access?` |
||||
|
}); |
||||
|
} |
||||
|
} |
Loading…
Reference in new issue