Browse Source

Task/move details of granted access from table to dialog on access page (#7792)

* Move details of granted access

* Update changelog
pull/7807/head
Thomas Kaul 5 days ago
committed by GitHub
parent
commit
72b0d80411
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 38
      apps/client/src/app/components/access-table/access-table.component.html
  3. 26
      apps/client/src/app/components/access-table/access-table.component.ts
  4. 37
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts
  5. 12
      apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html

1
CHANGELOG.md

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Moved the details of the granted access from the table to the dialog on the access page (experimental)
- Restricted the _Restricted view and manage_ permission of the access to share the portfolio to the Model Context Protocol (MCP) (experimental)
- Migrated the transfer cash balance dialog to a dedicated route
- Improved the language localization for Italian (`it`)

38
apps/client/src/app/components/access-table/access-table.component.html

@ -45,36 +45,6 @@
</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>
}
} @else if (element.type === 'MCP' && hasPermissionToEnableMcp) {
<div>
<code>{{ baseUrl }}{{ mcpEndpoint }}</code>
</div>
<div>
<code>Authorization: Bearer {{ element.id }}</code>
</div>
}
</td>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<th *matHeaderCellDef class="px-1 text-right" mat-header-cell></th>
@ -113,14 +83,6 @@
</span>
</button>
}
@if (element.type === 'MCP' && hasPermissionToEnableMcp) {
<button mat-menu-item (click)="onCopyIdToClipboard(element.id)">
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="copy-outline" />
<span i18n>Copy identifier to clipboard</span>
</span>
</button>
}
@if (
(!isReceivedAccess() && user()?.settings?.isExperimentalFeatures) ||
element.type === 'PUBLIC' ||

26
apps/client/src/app/components/access-table/access-table.component.ts

@ -1,4 +1,3 @@
import { MCP_ENDPOINT } from '@ghostfolio/common/config';
import { ConfirmationDialogType } from '@ghostfolio/common/enums';
import { getDateFormatString } from '@ghostfolio/common/helper';
import { Access, User } from '@ghostfolio/common/interfaces';
@ -32,7 +31,6 @@ import {
copyOutline,
createOutline,
ellipsisHorizontal,
linkOutline,
removeCircleOutline,
trashOutline
} from 'ionicons/icons';
@ -81,14 +79,7 @@ export class GfAccessTableComponent {
protected readonly dataSource = new MatTableDataSource<Access>();
protected readonly displayedColumns = computed(() => {
const columns = [
'alias',
'grantee',
'type',
'lastUsedAt',
'expiresAt',
'details'
];
const columns = ['alias', 'grantee', 'type', 'lastUsedAt', 'expiresAt'];
if (this.showActions()) {
columns.push('actions');
@ -109,8 +100,6 @@ export class GfAccessTableComponent {
return !this.accesses();
});
protected readonly mcpEndpoint = MCP_ENDPOINT;
private readonly clipboard = inject(Clipboard);
private readonly dataService = inject(DataService);
private readonly notificationService = inject(NotificationService);
@ -121,7 +110,6 @@ export class GfAccessTableComponent {
copyOutline,
createOutline,
ellipsisHorizontal,
linkOutline,
removeCircleOutline,
trashOutline
});
@ -142,18 +130,6 @@ export class GfAccessTableComponent {
return `${this.baseUrl}/${languageCode}/${publicRoutes.public.path}/${aId}`;
}
protected onCopyIdToClipboard(aId: string) {
this.clipboard.copy(aId);
this.snackBar.open(
'✅ ' + $localize`Identifier has been copied to the clipboard`,
undefined,
{
duration: ms('3 seconds')
}
);
}
protected onCopyUrlToClipboard(aId: string) {
this.clipboard.copy(this.getPublicUrl(aId));

37
apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts

@ -1,5 +1,5 @@
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { DEFAULT_LOCALE } from '@ghostfolio/common/config';
import { DEFAULT_LOCALE, MCP_ENDPOINT } from '@ghostfolio/common/config';
import { CreateAccessDto, UpdateAccessDto } from '@ghostfolio/common/dtos';
import { canApplyFiltersToAccess } from '@ghostfolio/common/helper';
import { Filter, PortfolioPosition } from '@ghostfolio/common/interfaces';
@ -27,6 +27,7 @@ import {
} from '@ghostfolio/ui/portfolio-filter-form';
import { DataService } from '@ghostfolio/ui/services';
import { JsonPipe } from '@angular/common';
import type { HttpErrorResponse } from '@angular/common/http';
import {
ChangeDetectionStrategy,
@ -69,6 +70,7 @@ import { CreateOrUpdateAccessDialogParams } from './interfaces/interfaces';
FormsModule,
GfAccessLevelIconComponent,
GfPortfolioFilterFormComponent,
JsonPipe,
MatButtonModule,
MatDatepickerModule,
MatDialogModule,
@ -88,6 +90,7 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
public tags: Filter[] = [];
protected accessForm: FormGroup;
protected readonly baseUrl = window.location.origin;
protected minExpiresAt: Date;
protected readonly mode: 'create' | 'update';
protected readonly today = startOfDay(new Date());
@ -222,6 +225,10 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
this.loadHoldings();
}
protected get accessId() {
return this.data.access?.id;
}
protected get accessLevel(): AccessLevel {
return this.accessForm?.get('accessLevel')?.value as AccessLevel;
}
@ -234,10 +241,32 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
return this.accessType === 'PUBLIC';
}
protected get mcpConfiguration() {
return {
headers: {
Authorization: `Bearer ${this.accessId}`
},
type: 'http',
url: `${this.baseUrl}${MCP_ENDPOINT}`
};
}
protected get showExpiresAtErrorMessage() {
return this.accessForm?.get('expiresAt')?.invalid === true;
}
protected get showMcpDetails() {
return this.canGrantMcpAccess && this.isMcpAccess && this.mode === 'update';
}
protected get showPublicDetails() {
return (
this.hasExperimentalFeatures &&
this.isPublicAccess &&
this.mode === 'update'
);
}
protected onCancel() {
this.dialogRef.close();
}
@ -250,6 +279,10 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
}
}
private get isMcpAccess() {
return this.accessType === 'MCP';
}
private async createAccess() {
const filters = this.getFilters();
@ -343,7 +376,7 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
}
private async updateAccess() {
const accessId = this.data.access?.id;
const accessId = this.accessId;
if (!accessId) {
return;

12
apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html

@ -133,6 +133,18 @@
[tags]="tags"
/>
}
@if (showMcpDetails || showPublicDetails) {
<h2 class="h6" i18n>Details</h2>
@if (showMcpDetails) {
<pre class="m-0"><code>{{ mcpConfiguration | json }}</code></pre>
} @else if (showPublicDetails) {
<div>
<code class="text-break"
>GET {{ baseUrl }}/api/v1/public/{{ accessId }}/portfolio</code
>
</div>
}
}
</div>
<div class="justify-content-end" mat-dialog-actions>
<button mat-button type="button" (click)="onCancel()">

Loading…
Cancel
Save