Browse Source

Task/improve style of settings tables in admin control panel (#7708)

* Improve style of settings table

* Update changelog
pull/7712/head
Thomas Kaul 4 days ago
committed by GitHub
parent
commit
b54e2ac144
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 3
      CHANGELOG.md
  2. 161
      apps/client/src/app/components/admin-platform/admin-platform.component.html
  3. 8
      apps/client/src/app/components/admin-platform/admin-platform.component.ts
  4. 326
      apps/client/src/app/components/admin-settings/admin-settings.component.html
  5. 1
      apps/client/src/app/components/admin-settings/admin-settings.component.ts
  6. 203
      apps/client/src/app/components/admin-tag/admin-tag.component.html

3
CHANGELOG.md

@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Improved the style of the table in the data providers management of the admin control panel
- Improved the style of the table in the platform management of the admin control panel
- Improved the style of the table in the tag management of the admin control panel
- Improved the historical market data gathering by storing the market prices carried forward for the most recent dates without data from the data provider, distinguished by `isCarriedForward`
- Introduced a cooldown of 12 hours for the historical market data gathering of a symbol by retaining the completed jobs

161
apps/client/src/app/components/admin-platform/admin-platform.component.html

@ -9,92 +9,99 @@
Add Platform
</a>
</div>
<table
class="gf-table w-100"
mat-table
matSort
matSortActive="name"
matSortDirection="asc"
[dataSource]="dataSource"
>
<ng-container matColumnDef="name">
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header="name">
<ng-container i18n>Name</ng-container>
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
<gf-entity-logo
class="d-inline mr-1"
[hasPlaceholder]="true"
[tooltip]="element.name"
[url]="element.url"
/>
<span>{{ element.name }}</span>
</td></ng-container
<div class="overflow-x-auto">
<table
class="gf-table w-100"
mat-table
matSort
matSortActive="name"
matSortDirection="asc"
[dataSource]="dataSource"
>
<ng-container matColumnDef="icon" sticky>
<th *matHeaderCellDef class="px-1" mat-header-cell></th>
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
<gf-entity-logo
[hasPlaceholder]="true"
[tooltip]="element.name"
[url]="element.url"
/>
</td>
</ng-container>
<ng-container matColumnDef="url">
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header="url">
<ng-container i18n>Url</ng-container>
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
{{ element.url }}
</td>
</ng-container>
<ng-container matColumnDef="name">
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header="name">
<ng-container i18n>Name</ng-container>
</th>
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell>
{{ element.name }}
</td>
</ng-container>
<ng-container matColumnDef="accounts">
<th
*matHeaderCellDef
class="justify-content-end px-1"
mat-header-cell
mat-sort-header="accountCount"
>
<ng-container i18n>Accounts</ng-container>
</th>
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
<gf-value
class="d-inline-block justify-content-end"
[locale]="locale()"
[value]="element.accountCount"
/>
</td>
</ng-container>
<ng-container matColumnDef="url">
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header="url">
<ng-container i18n>Url</ng-container>
</th>
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell>
{{ element.url }}
</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]="platformMenu"
(click)="$event.stopPropagation()"
<ng-container matColumnDef="accounts">
<th
*matHeaderCellDef
class="justify-content-end px-1"
mat-header-cell
mat-sort-header="accountCount"
>
<ion-icon name="ellipsis-horizontal" />
</button>
<mat-menu #platformMenu="matMenu" xPosition="before">
<button mat-menu-item (click)="onUpdatePlatform(element)">
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="create-outline" />
<span><ng-container i18n>Edit</ng-container>...</span>
</span>
</button>
<hr class="m-0" />
<ng-container i18n>Accounts</ng-container>
</th>
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
<gf-value
class="d-inline-block justify-content-end"
[locale]="locale()"
[value]="element.accountCount"
/>
</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
mat-menu-item
[disabled]="element.accountCount > 0"
(click)="onDeletePlatform(element.id)"
class="mx-1 no-min-width px-2"
mat-button
[matMenuTriggerFor]="platformMenu"
(click)="$event.stopPropagation()"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="trash-outline" />
<span i18n>Delete</span>
</span>
<ion-icon name="ellipsis-horizontal" />
</button>
</mat-menu>
</td>
</ng-container>
<mat-menu #platformMenu="matMenu" xPosition="before">
<button mat-menu-item (click)="onUpdatePlatform(element)">
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="create-outline" />
<span><ng-container i18n>Edit</ng-container>...</span>
</span>
</button>
<hr class="m-0" />
<button
mat-menu-item
[disabled]="element.accountCount > 0"
(click)="onDeletePlatform(element.id)"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="trash-outline" />
<span i18n>Delete</span>
</span>
</button>
</mat-menu>
</td>
</ng-container>
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
</table>
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
</table>
</div>
<mat-paginator
[class.d-none]="dataSource.data.length <= pageSize"

8
apps/client/src/app/components/admin-platform/admin-platform.component.ts

@ -61,7 +61,13 @@ export class GfAdminPlatformComponent implements OnInit {
public readonly locale = input(getLocale());
protected dataSource = new MatTableDataSource<Platform>();
protected readonly displayedColumns = ['name', 'url', 'accounts', 'actions'];
protected readonly displayedColumns = [
'icon',
'name',
'url',
'accounts',
'actions'
];
protected readonly pageSize = DEFAULT_PAGE_SIZE;
protected platforms: Platform[];

326
apps/client/src/app/components/admin-settings/admin-settings.component.html

@ -40,179 +40,181 @@
</mat-card-actions>
</mat-card>
}
<table
class="gf-table w-100"
mat-table
matSort
matSortActive="name"
matSortDirection="asc"
[dataSource]="dataSource"
>
<ng-container matColumnDef="name">
<th
*matHeaderCellDef
class="px-1"
mat-header-cell
mat-sort-header="name"
>
<ng-container i18n>Name</ng-container>
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
<div class="d-flex align-items-center">
<gf-entity-logo
class="mr-1"
[hasPlaceholder]="true"
[url]="element.url"
/>
<div>
@if (isGhostfolioDataProvider(element)) {
<a
class="align-items-center d-inline-flex"
target="_blank"
[href]="pricingUrl"
>
Ghostfolio Premium
<gf-premium-indicator
class="d-inline-block ml-1"
[enableLink]="false"
/>
@if (isGhostfolioApiKeyValid === false) {
<span
class="badge badge-pill badge-secondary ml-2 text-uppercase"
i18n
>popular</span
>
}
</a>
@if (isGhostfolioApiKeyValid === true) {
<div class="line-height-1">
<small class="text-muted">
<ng-container i18n>Valid until</ng-container>
{{
ghostfolioApiStatus?.subscription?.expiresAt
| date: defaultDateFormat
}}
</small>
</div>
<div class="overflow-x-auto">
<table
class="gf-table w-100"
mat-table
matSort
matSortActive="name"
matSortDirection="asc"
[dataSource]="dataSource"
>
<ng-container matColumnDef="icon" sticky>
<th *matHeaderCellDef class="px-1" mat-header-cell></th>
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
<gf-entity-logo [hasPlaceholder]="true" [url]="element.url" />
</td>
</ng-container>
<ng-container matColumnDef="name">
<th
*matHeaderCellDef
class="px-1"
mat-header-cell
mat-sort-header="name"
>
<ng-container i18n>Name</ng-container>
</th>
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell>
@if (isGhostfolioDataProvider(element)) {
<a
class="align-items-center d-inline-flex"
target="_blank"
[href]="pricingUrl"
>
Ghostfolio Premium
<gf-premium-indicator
class="d-inline-block ml-1"
[enableLink]="false"
/>
@if (isGhostfolioApiKeyValid === false) {
<span
class="badge badge-pill badge-secondary ml-2 text-uppercase"
i18n
>popular</span
>
}
} @else {
{{ element.name }}
</a>
@if (isGhostfolioApiKeyValid === true) {
<div class="line-height-1">
<small class="text-muted">
<ng-container i18n>Valid until</ng-container>
{{
ghostfolioApiStatus?.subscription?.expiresAt
| date: defaultDateFormat
}}
</small>
</div>
}
</div>
</div>
</td>
</ng-container>
} @else {
{{ element.name }}
}
</td>
</ng-container>
<ng-container matColumnDef="status">
<th *matHeaderCellDef class="px-1" mat-header-cell>
<ng-container i18n>Status</ng-container>
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
@if (
hasGhostfolioApiKey &&
isGhostfolioApiKeyValid === false &&
isGhostfolioDataProvider(element)
) {
<span class="text-danger" i18n>Invalid API key</span>
} @else if (
hasGhostfolioApiKey || !isGhostfolioDataProvider(element)
) {
<gf-data-provider-status [dataSource]="element.dataSource" />
}
</td>
</ng-container>
<ng-container matColumnDef="status">
<th *matHeaderCellDef class="px-1" mat-header-cell>
<ng-container i18n>Status</ng-container>
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
@if (
hasGhostfolioApiKey &&
isGhostfolioApiKeyValid === false &&
isGhostfolioDataProvider(element)
) {
<span class="text-danger" i18n>Invalid API key</span>
} @else if (
hasGhostfolioApiKey || !isGhostfolioDataProvider(element)
) {
<gf-data-provider-status [dataSource]="element.dataSource" />
}
</td>
</ng-container>
<ng-container matColumnDef="assetProfileCount">
<th
*matHeaderCellDef
class="justify-content-end px-1"
mat-header-cell
mat-sort-header="assetProfileCount"
>
<ng-container i18n>Asset Profiles</ng-container>
</th>
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
<gf-value
class="d-inline-block justify-content-end"
[locale]="user?.settings?.locale"
[value]="element.assetProfileCount"
/>
</td>
</ng-container>
<ng-container matColumnDef="assetProfileCount">
<th
*matHeaderCellDef
class="justify-content-end px-1"
mat-header-cell
mat-sort-header="assetProfileCount"
>
<ng-container i18n>Asset Profiles</ng-container>
</th>
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
<gf-value
class="d-inline-block justify-content-end"
[locale]="user?.settings?.locale"
[value]="element.assetProfileCount"
/>
</td>
</ng-container>
<ng-container matColumnDef="usage">
<th *matHeaderCellDef class="px-1" mat-header-cell></th>
<td *matCellDef="let element" class="px-1" mat-cell>
@if (
isGhostfolioDataProvider(element) &&
isGhostfolioApiKeyValid === true
) {
<div
[matTooltip]="ghostfolioApiStatusTooltip"
[matTooltipDisabled]="!ghostfolioApiStatus.isWithinSetupPeriod"
>
<mat-progress-bar
mode="determinate"
[value]="
100 -
(ghostfolioApiStatus.dailyRequests /
ghostfolioApiStatus.dailyRequestsMax) *
100
<ng-container matColumnDef="usage">
<th *matHeaderCellDef class="px-1" mat-header-cell></th>
<td *matCellDef="let element" class="px-1" mat-cell>
@if (
isGhostfolioDataProvider(element) &&
isGhostfolioApiKeyValid === true
) {
<div
[matTooltip]="ghostfolioApiStatusTooltip"
[matTooltipDisabled]="
!ghostfolioApiStatus.isWithinSetupPeriod
"
/>
<small class="text-muted">
{{ ghostfolioApiStatus.dailyRequests }}
<ng-container i18n>of</ng-container>
{{ ghostfolioApiStatus.dailyRequestsMax }}
<ng-container i18n>daily requests</ng-container>
</small>
</div>
}
</td>
</ng-container>
>
<mat-progress-bar
mode="determinate"
[value]="
100 -
(ghostfolioApiStatus.dailyRequests /
ghostfolioApiStatus.dailyRequestsMax) *
100
"
/>
<small class="text-muted">
{{ ghostfolioApiStatus.dailyRequests }}
<ng-container i18n>of</ng-container>
{{ ghostfolioApiStatus.dailyRequestsMax }}
<ng-container i18n>daily requests</ng-container>
</small>
</div>
}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th *matHeaderCellDef class="px-1" mat-header-cell></th>
<ng-container matColumnDef="actions">
<th *matHeaderCellDef class="px-1" mat-header-cell></th>
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
@if (isGhostfolioDataProvider(element)) {
@if (hasGhostfolioApiKey) {
<button
class="mx-1 no-min-width px-2"
mat-button
[matMenuTriggerFor]="ghostfolioApiMenu"
(click)="$event.stopPropagation()"
>
<ion-icon name="ellipsis-horizontal" />
</button>
<mat-menu
#ghostfolioApiMenu="matMenu"
class="no-max-width"
xPosition="before"
>
<button mat-menu-item (click)="onRemoveGhostfolioApiKey()">
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="trash-outline" />
<span i18n>Remove API key</span>
</span>
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
@if (isGhostfolioDataProvider(element)) {
@if (hasGhostfolioApiKey) {
<button
class="mx-1 no-min-width px-2"
mat-button
[matMenuTriggerFor]="ghostfolioApiMenu"
(click)="$event.stopPropagation()"
>
<ion-icon name="ellipsis-horizontal" />
</button>
</mat-menu>
} @else if (hasGhostfolioApiKey === false) {
<button
class="special"
mat-flat-button
(click)="onSetGhostfolioApiKey()"
>
<ng-container i18n>Set API key</ng-container>
</button>
<mat-menu
#ghostfolioApiMenu="matMenu"
class="no-max-width"
xPosition="before"
>
<button mat-menu-item (click)="onRemoveGhostfolioApiKey()">
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="trash-outline" />
<span i18n>Remove API key</span>
</span>
</button>
</mat-menu>
} @else if (hasGhostfolioApiKey === false) {
<button
class="special text-nowrap"
mat-flat-button
(click)="onSetGhostfolioApiKey()"
>
<ng-container i18n>Set API key</ng-container>
</button>
}
}
}
</td>
</ng-container>
</td>
</ng-container>
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
</table>
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
</table>
</div>
@if (isLoading) {
<ngx-skeleton-loader
animation="pulse"

1
apps/client/src/app/components/admin-settings/admin-settings.component.ts

@ -76,6 +76,7 @@ export class GfAdminSettingsComponent implements OnInit {
public dataSource = new MatTableDataSource<DataProviderInfo>();
public defaultDateFormat: string;
public displayedColumns = [
'icon',
'name',
'status',
'assetProfileCount',

203
apps/client/src/app/components/admin-tag/admin-tag.component.html

@ -9,112 +9,119 @@
Add Tag
</a>
</div>
<table
class="gf-table w-100"
mat-table
matSort
matSortActive="name"
matSortDirection="asc"
[dataSource]="dataSource"
>
<ng-container matColumnDef="name">
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header="name">
<ng-container i18n>Name</ng-container>
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
{{ translate(element.name) }}
</td>
</ng-container>
<ng-container matColumnDef="userId">
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header="userId">
<ng-container i18n>User</ng-container>
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
<span class="text-monospace">{{ element.userId }}</span>
</td>
</ng-container>
<div class="overflow-x-auto">
<table
class="gf-table w-100"
mat-table
matSort
matSortActive="name"
matSortDirection="asc"
[dataSource]="dataSource"
>
<ng-container matColumnDef="name">
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header="name">
<ng-container i18n>Name</ng-container>
</th>
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell>
{{ translate(element.name) }}
</td>
</ng-container>
<ng-container matColumnDef="accounts">
<th
*matHeaderCellDef
class="justify-content-end px-1"
mat-header-cell
mat-sort-header="accountCount"
>
<ng-container i18n>Accounts</ng-container>
</th>
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
<gf-value
class="d-inline-block justify-content-end"
[locale]="locale()"
[value]="element.accountCount"
/>
</td>
</ng-container>
<ng-container matColumnDef="userId">
<th
*matHeaderCellDef
class="px-1"
mat-header-cell
mat-sort-header="userId"
>
<ng-container i18n>User</ng-container>
</th>
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell>
<span class="text-monospace">{{ element.userId }}</span>
</td>
</ng-container>
<ng-container matColumnDef="activities">
<th
*matHeaderCellDef
class="justify-content-end px-1"
mat-header-cell
mat-sort-header="activityCount"
>
<ng-container i18n>Activities</ng-container>
</th>
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
<gf-value
class="d-inline-block justify-content-end"
[locale]="locale()"
[value]="element.activityCount"
/>
</td>
</ng-container>
<ng-container matColumnDef="accounts">
<th
*matHeaderCellDef
class="justify-content-end px-1"
mat-header-cell
mat-sort-header="accountCount"
>
<ng-container i18n>Accounts</ng-container>
</th>
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
<gf-value
class="d-inline-block justify-content-end"
[locale]="locale()"
[value]="element.accountCount"
/>
</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]="tagMenu"
(click)="$event.stopPropagation()"
<ng-container matColumnDef="activities">
<th
*matHeaderCellDef
class="justify-content-end px-1"
mat-header-cell
mat-sort-header="activityCount"
>
<ion-icon name="ellipsis-horizontal" />
</button>
<mat-menu #tagMenu="matMenu" xPosition="before">
<button
mat-menu-item
[disabled]="isSystemTag(element)"
(click)="onUpdateTag(element)"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="create-outline" />
<span><ng-container i18n>Edit</ng-container>...</span>
</span>
</button>
<hr class="m-0" />
<ng-container i18n>Activities</ng-container>
</th>
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
<gf-value
class="d-inline-block justify-content-end"
[locale]="locale()"
[value]="element.activityCount"
/>
</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
mat-menu-item
[disabled]="
element.accountCount > 0 ||
element.activityCount > 0 ||
isSystemTag(element)
"
(click)="onDeleteTag(element.id)"
class="mx-1 no-min-width px-2"
mat-button
[matMenuTriggerFor]="tagMenu"
(click)="$event.stopPropagation()"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="trash-outline" />
<span i18n>Delete</span>
</span>
<ion-icon name="ellipsis-horizontal" />
</button>
</mat-menu>
</td>
</ng-container>
<mat-menu #tagMenu="matMenu" xPosition="before">
<button
mat-menu-item
[disabled]="isSystemTag(element)"
(click)="onUpdateTag(element)"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="create-outline" />
<span><ng-container i18n>Edit</ng-container>...</span>
</span>
</button>
<hr class="m-0" />
<button
mat-menu-item
[disabled]="
element.accountCount > 0 ||
element.activityCount > 0 ||
isSystemTag(element)
"
(click)="onDeleteTag(element.id)"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="trash-outline" />
<span i18n>Delete</span>
</span>
</button>
</mat-menu>
</td>
</ng-container>
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
</table>
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
</table>
</div>
<mat-paginator
[class.d-none]="dataSource.data.length <= pageSize"

Loading…
Cancel
Save