core: show all applications a user can access in admin interface (#8343)
* core: show all applications a user can access in admin interface Signed-off-by: Jens Langhammer <jens@goauthentik.io> * minor adjustments Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add relative time Signed-off-by: Jens Langhammer <jens@goauthentik.io> * use relative time in most places Signed-off-by: Jens Langhammer <jens@goauthentik.io> * improve admin dashboard scaling Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
@ -11,6 +11,7 @@ from rest_framework.permissions import AllowAny
|
|||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
|
from rest_framework.validators import UniqueValidator
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from authentik.api.authorization import SecretKeyFilter
|
from authentik.api.authorization import SecretKeyFilter
|
||||||
@ -57,6 +58,11 @@ class BrandSerializer(ModelSerializer):
|
|||||||
"web_certificate",
|
"web_certificate",
|
||||||
"attributes",
|
"attributes",
|
||||||
]
|
]
|
||||||
|
extra_kwargs = {
|
||||||
|
# TODO: This field isn't unique on the database which is hard to backport
|
||||||
|
# hence we just validate the uniqueness here
|
||||||
|
"domain": {"validators": [UniqueValidator(Brand.objects.all())]},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class Themes(models.TextChoices):
|
class Themes(models.TextChoices):
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
"""Application API Views"""
|
"""Application API Views"""
|
||||||
|
from copy import copy
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@ -128,10 +129,16 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
|||||||
queryset = backend().filter_queryset(self.request, queryset, self)
|
queryset = backend().filter_queryset(self.request, queryset, self)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def _get_allowed_applications(self, queryset: QuerySet) -> list[Application]:
|
def _get_allowed_applications(
|
||||||
|
self, queryset: QuerySet, user: Optional[User] = None
|
||||||
|
) -> list[Application]:
|
||||||
applications = []
|
applications = []
|
||||||
|
request = self.request._request
|
||||||
|
if user:
|
||||||
|
request = copy(request)
|
||||||
|
request.user = user
|
||||||
for application in queryset:
|
for application in queryset:
|
||||||
engine = PolicyEngine(application, self.request.user, self.request)
|
engine = PolicyEngine(application, request.user, request)
|
||||||
engine.build()
|
engine.build()
|
||||||
if engine.passing:
|
if engine.passing:
|
||||||
applications.append(application)
|
applications.append(application)
|
||||||
@ -188,20 +195,43 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
|||||||
name="superuser_full_list",
|
name="superuser_full_list",
|
||||||
location=OpenApiParameter.QUERY,
|
location=OpenApiParameter.QUERY,
|
||||||
type=OpenApiTypes.BOOL,
|
type=OpenApiTypes.BOOL,
|
||||||
)
|
),
|
||||||
|
OpenApiParameter(
|
||||||
|
name="for_user",
|
||||||
|
location=OpenApiParameter.QUERY,
|
||||||
|
type=OpenApiTypes.INT,
|
||||||
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
def list(self, request: Request) -> Response:
|
def list(self, request: Request) -> Response:
|
||||||
"""Custom list method that checks Policy based access instead of guardian"""
|
"""Custom list method that checks Policy based access instead of guardian"""
|
||||||
should_cache = request.GET.get("search", "") == ""
|
should_cache = request.query_params.get("search", "") == ""
|
||||||
|
|
||||||
superuser_full_list = str(request.GET.get("superuser_full_list", "false")).lower() == "true"
|
superuser_full_list = (
|
||||||
|
str(request.query_params.get("superuser_full_list", "false")).lower() == "true"
|
||||||
|
)
|
||||||
if superuser_full_list and request.user.is_superuser:
|
if superuser_full_list and request.user.is_superuser:
|
||||||
return super().list(request)
|
return super().list(request)
|
||||||
|
|
||||||
queryset = self._filter_queryset_for_list(self.get_queryset())
|
queryset = self._filter_queryset_for_list(self.get_queryset())
|
||||||
self.paginate_queryset(queryset)
|
self.paginate_queryset(queryset)
|
||||||
|
|
||||||
|
if "for_user" in request.query_params:
|
||||||
|
try:
|
||||||
|
for_user: int = int(request.query_params.get("for_user", 0))
|
||||||
|
for_user = (
|
||||||
|
get_objects_for_user(request.user, "authentik_core.view_user_applications")
|
||||||
|
.filter(pk=for_user)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not for_user:
|
||||||
|
raise ValidationError({"for_user": "User not found"})
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ValidationError from exc
|
||||||
|
allowed_applications = self._get_allowed_applications(queryset, user=for_user)
|
||||||
|
serializer = self.get_serializer(allowed_applications, many=True)
|
||||||
|
return self.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
allowed_applications = []
|
allowed_applications = []
|
||||||
if not should_cache:
|
if not should_cache:
|
||||||
allowed_applications = self._get_allowed_applications(queryset)
|
allowed_applications = self._get_allowed_applications(queryset)
|
||||||
|
12
schema.yml
12
schema.yml
@ -2658,6 +2658,10 @@ paths:
|
|||||||
operationId: core_applications_list
|
operationId: core_applications_list
|
||||||
description: Custom list method that checks Policy based access instead of guardian
|
description: Custom list method that checks Policy based access instead of guardian
|
||||||
parameters:
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: for_user
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
- in: query
|
- in: query
|
||||||
name: group
|
name: group
|
||||||
schema:
|
schema:
|
||||||
@ -2931,8 +2935,14 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/PolicyTestResult'
|
$ref: '#/components/schemas/PolicyTestResult'
|
||||||
description: ''
|
description: ''
|
||||||
|
'404':
|
||||||
|
description: for_user user not found
|
||||||
'400':
|
'400':
|
||||||
description: Bad request
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ValidationError'
|
||||||
|
description: ''
|
||||||
'403':
|
'403':
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
|
@ -79,7 +79,9 @@ export class AdminOverviewPage extends AKElement {
|
|||||||
<section class="pf-c-page__main-section">
|
<section class="pf-c-page__main-section">
|
||||||
<div class="pf-l-grid pf-m-gutter">
|
<div class="pf-l-grid pf-m-gutter">
|
||||||
<!-- row 1 -->
|
<!-- row 1 -->
|
||||||
<div class="pf-l-grid__item pf-m-6-col pf-l-grid pf-m-gutter">
|
<div
|
||||||
|
class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-6-col-on-2xl pf-l-grid pf-m-gutter"
|
||||||
|
>
|
||||||
<div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-4-col-on-2xl">
|
<div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-4-col-on-2xl">
|
||||||
<ak-aggregate-card
|
<ak-aggregate-card
|
||||||
icon="fa fa-share"
|
icon="fa fa-share"
|
||||||
@ -167,7 +169,7 @@ export class AdminOverviewPage extends AKElement {
|
|||||||
<ak-admin-status-card-workers> </ak-admin-status-card-workers>
|
<ak-admin-status-card-workers> </ak-admin-status-card-workers>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-l-grid__item pf-m-6-col">
|
<div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl">
|
||||||
<ak-recent-events pageSize="6"></ak-recent-events>
|
<ak-recent-events pageSize="6"></ak-recent-events>
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-l-grid__item pf-m-12-col">
|
<div class="pf-l-grid__item pf-m-12-col">
|
||||||
|
@ -47,6 +47,9 @@ export class RecentEventsCard extends Table<Event> {
|
|||||||
--pf-c-card__title--FontSize: var(--pf-global--FontSize--md);
|
--pf-c-card__title--FontSize: var(--pf-global--FontSize--md);
|
||||||
--pf-c-card__title--FontWeight: var(--pf-global--FontWeight--bold);
|
--pf-c-card__title--FontWeight: var(--pf-global--FontWeight--bold);
|
||||||
}
|
}
|
||||||
|
* {
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,6 @@ import "@goauthentik/elements/buttons/SpinnerButton";
|
|||||||
import "@goauthentik/elements/forms/DeleteBulkForm";
|
import "@goauthentik/elements/forms/DeleteBulkForm";
|
||||||
import "@goauthentik/elements/forms/ModalForm";
|
import "@goauthentik/elements/forms/ModalForm";
|
||||||
import { getURLParam } from "@goauthentik/elements/router/RouteMatch";
|
import { getURLParam } from "@goauthentik/elements/router/RouteMatch";
|
||||||
// import { getURLParam } from "@goauthentik/elements/router/RouteMatch";
|
|
||||||
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||||
import { TableColumn } from "@goauthentik/elements/table/Table";
|
import { TableColumn } from "@goauthentik/elements/table/Table";
|
||||||
import { TablePage } from "@goauthentik/elements/table/TablePage";
|
import { TablePage } from "@goauthentik/elements/table/TablePage";
|
||||||
@ -25,6 +24,22 @@ import { Application, CoreApi } from "@goauthentik/api";
|
|||||||
|
|
||||||
import "./ApplicationWizardHint";
|
import "./ApplicationWizardHint";
|
||||||
|
|
||||||
|
export const applicationListStyle = css`
|
||||||
|
/* Fix alignment issues with images in tables */
|
||||||
|
.pf-c-table tbody > tr > * {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
tr td:first-child {
|
||||||
|
width: auto;
|
||||||
|
min-width: 0px;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.pf-c-sidebar.pf-m-gutter > .pf-c-sidebar__main > * + * {
|
||||||
|
margin-left: calc(var(--pf-c-sidebar__main--child--MarginLeft) / 2);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
@customElement("ak-application-list")
|
@customElement("ak-application-list")
|
||||||
export class ApplicationListPage extends TablePage<Application> {
|
export class ApplicationListPage extends TablePage<Application> {
|
||||||
searchEnabled(): boolean {
|
searchEnabled(): boolean {
|
||||||
@ -59,24 +74,7 @@ export class ApplicationListPage extends TablePage<Application> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return super.styles.concat(
|
return super.styles.concat(PFCard, applicationListStyle);
|
||||||
PFCard,
|
|
||||||
css`
|
|
||||||
/* Fix alignment issues with images in tables */
|
|
||||||
.pf-c-table tbody > tr > * {
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
tr td:first-child {
|
|
||||||
width: auto;
|
|
||||||
min-width: 0px;
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.pf-c-sidebar.pf-m-gutter > .pf-c-sidebar__main > * + * {
|
|
||||||
margin-left: calc(var(--pf-c-sidebar__main--child--MarginLeft) / 2);
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
columns(): TableColumn[] {
|
columns(): TableColumn[] {
|
||||||
@ -97,7 +95,6 @@ export class ApplicationListPage extends TablePage<Application> {
|
|||||||
renderSidebarAfter(): TemplateResult {
|
renderSidebarAfter(): TemplateResult {
|
||||||
// Rendering the wizard with .open here, as if we set the attribute in
|
// Rendering the wizard with .open here, as if we set the attribute in
|
||||||
// renderObjectCreate() it'll open two wizards, since that function gets called twice
|
// renderObjectCreate() it'll open two wizards, since that function gets called twice
|
||||||
|
|
||||||
return html`<div class="pf-c-sidebar__panel pf-m-width-25">
|
return html`<div class="pf-c-sidebar__panel pf-m-width-25">
|
||||||
<div class="pf-c-card">
|
<div class="pf-c-card">
|
||||||
<div class="pf-c-card__body">
|
<div class="pf-c-card__body">
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import "@goauthentik/admin/blueprints/BlueprintForm";
|
import "@goauthentik/admin/blueprints/BlueprintForm";
|
||||||
|
import { getRelativeTime } from "@goauthentik/app/common/utils";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||||
import { uiConfig } from "@goauthentik/common/ui/config";
|
import { uiConfig } from "@goauthentik/common/ui/config";
|
||||||
@ -144,7 +145,8 @@ export class BlueprintListPage extends TablePage<BlueprintInstance> {
|
|||||||
html`<div>${item.name}</div>
|
html`<div>${item.name}</div>
|
||||||
${description ? html`<small>${description}</small>` : html``}`,
|
${description ? html`<small>${description}</small>` : html``}`,
|
||||||
html`${BlueprintStatus(item)}`,
|
html`${BlueprintStatus(item)}`,
|
||||||
html`${item.lastApplied.toLocaleString()}`,
|
html`<div>${getRelativeTime(item.lastApplied)}</div>
|
||||||
|
<small>${item.lastApplied.toLocaleString()}</small>`,
|
||||||
html`<ak-status-label ?good=${item.enabled}></ak-status-label>`,
|
html`<ak-status-label ?good=${item.enabled}></ak-status-label>`,
|
||||||
html`<ak-forms-modal>
|
html`<ak-forms-modal>
|
||||||
<span slot="submit"> ${msg("Update")} </span>
|
<span slot="submit"> ${msg("Update")} </span>
|
||||||
|
@ -30,7 +30,7 @@ export class BrandListPage extends TablePage<Brand> {
|
|||||||
return msg("Configure visual settings and defaults for different domains.");
|
return msg("Configure visual settings and defaults for different domains.");
|
||||||
}
|
}
|
||||||
pageIcon(): string {
|
pageIcon(): string {
|
||||||
return "pf-icon pf-icon-brand";
|
return "pf-icon pf-icon-tenant";
|
||||||
}
|
}
|
||||||
|
|
||||||
checkbox = true;
|
checkbox = true;
|
||||||
@ -51,6 +51,7 @@ export class BrandListPage extends TablePage<Brand> {
|
|||||||
columns(): TableColumn[] {
|
columns(): TableColumn[] {
|
||||||
return [
|
return [
|
||||||
new TableColumn(msg("Domain"), "domain"),
|
new TableColumn(msg("Domain"), "domain"),
|
||||||
|
new TableColumn(msg("Brand name"), "branding_title"),
|
||||||
new TableColumn(msg("Default?"), "default"),
|
new TableColumn(msg("Default?"), "default"),
|
||||||
new TableColumn(msg("Actions")),
|
new TableColumn(msg("Actions")),
|
||||||
];
|
];
|
||||||
@ -84,6 +85,7 @@ export class BrandListPage extends TablePage<Brand> {
|
|||||||
row(item: Brand): TemplateResult[] {
|
row(item: Brand): TemplateResult[] {
|
||||||
return [
|
return [
|
||||||
html`${item.domain}`,
|
html`${item.domain}`,
|
||||||
|
html`${item.brandingTitle}`,
|
||||||
html`<ak-status-label ?good=${item._default}></ak-status-label>`,
|
html`<ak-status-label ?good=${item._default}></ak-status-label>`,
|
||||||
html`<ak-forms-modal>
|
html`<ak-forms-modal>
|
||||||
<span slot="submit"> ${msg("Update")} </span>
|
<span slot="submit"> ${msg("Update")} </span>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import "@goauthentik/admin/enterprise/EnterpriseLicenseForm";
|
import "@goauthentik/admin/enterprise/EnterpriseLicenseForm";
|
||||||
|
import { getRelativeTime } from "@goauthentik/app/common/utils";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { uiConfig } from "@goauthentik/common/ui/config";
|
import { uiConfig } from "@goauthentik/common/ui/config";
|
||||||
import { PFColor } from "@goauthentik/elements/Label";
|
import { PFColor } from "@goauthentik/elements/Label";
|
||||||
@ -202,7 +203,8 @@ export class EnterpriseLicenseListPage extends TablePage<License> {
|
|||||||
subtext=${msg("Cumulative license expiry")}
|
subtext=${msg("Cumulative license expiry")}
|
||||||
>
|
>
|
||||||
${this.summary?.hasLicense
|
${this.summary?.hasLicense
|
||||||
? this.summary.latestValid.toLocaleString()
|
? html`<div>${getRelativeTime(this.summary.latestValid)}</div>
|
||||||
|
<small>${this.summary.latestValid.toLocaleString()}</small>`
|
||||||
: "-"}
|
: "-"}
|
||||||
</ak-aggregate-card>
|
</ak-aggregate-card>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { uiConfig } from "@goauthentik/common/ui/config";
|
import { uiConfig } from "@goauthentik/common/ui/config";
|
||||||
import { first } from "@goauthentik/common/utils";
|
import { getRelativeTime } from "@goauthentik/common/utils";
|
||||||
import "@goauthentik/components/ak-status-label";
|
import "@goauthentik/components/ak-status-label";
|
||||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||||
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||||
@ -49,7 +49,10 @@ export class MemberSelectTable extends TableModal<User> {
|
|||||||
html`<div>${item.username}</div>
|
html`<div>${item.username}</div>
|
||||||
<small>${item.name}</small>`,
|
<small>${item.name}</small>`,
|
||||||
html` <ak-status-label type="warning" ?good=${item.isActive}></ak-status-label>`,
|
html` <ak-status-label type="warning" ?good=${item.isActive}></ak-status-label>`,
|
||||||
html`${first(item.lastLogin?.toLocaleString(), msg("-"))}`,
|
html`${item.lastLogin
|
||||||
|
? html`<div>${getRelativeTime(item.lastLogin)}</div>
|
||||||
|
<small>${item.lastLogin.toLocaleString()}</small>`
|
||||||
|
: msg("-")}`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import { me } from "@goauthentik/app/common/users";
|
|||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { MessageLevel } from "@goauthentik/common/messages";
|
import { MessageLevel } from "@goauthentik/common/messages";
|
||||||
import { uiConfig } from "@goauthentik/common/ui/config";
|
import { uiConfig } from "@goauthentik/common/ui/config";
|
||||||
import { first } from "@goauthentik/common/utils";
|
import { getRelativeTime } from "@goauthentik/common/utils";
|
||||||
import "@goauthentik/components/ak-status-label";
|
import "@goauthentik/components/ak-status-label";
|
||||||
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
|
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
|
||||||
import {
|
import {
|
||||||
@ -199,7 +199,10 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
|
|||||||
<small>${item.name}</small>
|
<small>${item.name}</small>
|
||||||
</a>`,
|
</a>`,
|
||||||
html`<ak-status-label ?good=${item.isActive}></ak-status-label>`,
|
html`<ak-status-label ?good=${item.isActive}></ak-status-label>`,
|
||||||
html`${first(item.lastLogin?.toLocaleString(), msg("-"))}`,
|
html`${item.lastLogin
|
||||||
|
? html`<div>${getRelativeTime(item.lastLogin)}</div>
|
||||||
|
<small>${item.lastLogin.toLocaleString()}</small>`
|
||||||
|
: msg("-")}`,
|
||||||
html`<ak-forms-modal>
|
html`<ak-forms-modal>
|
||||||
<span slot="submit"> ${msg("Update")} </span>
|
<span slot="submit"> ${msg("Update")} </span>
|
||||||
<span slot="header"> ${msg("Update User")} </span>
|
<span slot="header"> ${msg("Update User")} </span>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { getRelativeTime } from "@goauthentik/app/common/utils";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { uiConfig } from "@goauthentik/common/ui/config";
|
import { uiConfig } from "@goauthentik/common/ui/config";
|
||||||
import "@goauthentik/elements/buttons/ModalButton";
|
import "@goauthentik/elements/buttons/ModalButton";
|
||||||
@ -92,7 +93,8 @@ export class ReputationListPage extends TablePage<Reputation> {
|
|||||||
: html``}
|
: html``}
|
||||||
${item.ip}`,
|
${item.ip}`,
|
||||||
html`${item.score}`,
|
html`${item.score}`,
|
||||||
html`${item.updated.toLocaleString()}`,
|
html`<div>${getRelativeTime(item.updated)}</div>
|
||||||
|
<small>${item.updated.toLocaleString()}</small>`,
|
||||||
html`
|
html`
|
||||||
<ak-rbac-object-permission-modal
|
<ak-rbac-object-permission-modal
|
||||||
model=${RbacPermissionsAssignedByUsersListModelEnum.PoliciesReputationReputationpolicy}
|
model=${RbacPermissionsAssignedByUsersListModelEnum.PoliciesReputationReputationpolicy}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { uiConfig } from "@goauthentik/app/common/ui/config";
|
import { uiConfig } from "@goauthentik/app/common/ui/config";
|
||||||
|
import { getRelativeTime } from "@goauthentik/app/common/utils";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||||
import { PFColor } from "@goauthentik/elements/Label";
|
import { PFColor } from "@goauthentik/elements/Label";
|
||||||
@ -111,7 +112,8 @@ export class SystemTaskListPage extends TablePage<SystemTask> {
|
|||||||
return [
|
return [
|
||||||
html`${item.name}${item.uid ? `:${item.uid}` : ""}`,
|
html`${item.name}${item.uid ? `:${item.uid}` : ""}`,
|
||||||
html`${item.description}`,
|
html`${item.description}`,
|
||||||
html`${item.finishTimestamp.toLocaleString()}`,
|
html`<div>${getRelativeTime(item.finishTimestamp)}</div>
|
||||||
|
<small>${item.finishTimestamp.toLocaleString()}</small>`,
|
||||||
this.taskStatus(item),
|
this.taskStatus(item),
|
||||||
html`<ak-action-button
|
html`<ak-action-button
|
||||||
class="pf-m-plain"
|
class="pf-m-plain"
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import "@goauthentik/admin/tokens/TokenForm";
|
import "@goauthentik/admin/tokens/TokenForm";
|
||||||
|
import { getRelativeTime } from "@goauthentik/app/common/utils";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { intentToLabel } from "@goauthentik/common/labels";
|
import { intentToLabel } from "@goauthentik/common/labels";
|
||||||
import { uiConfig } from "@goauthentik/common/ui/config";
|
import { uiConfig } from "@goauthentik/common/ui/config";
|
||||||
@ -111,7 +112,10 @@ export class TokenListPage extends TablePage<Token> {
|
|||||||
: html``}`,
|
: html``}`,
|
||||||
html`<a href="#/identity/users/${item.userObj?.pk}">${item.userObj?.username}</a>`,
|
html`<a href="#/identity/users/${item.userObj?.pk}">${item.userObj?.username}</a>`,
|
||||||
html`<ak-status-label type="warning" ?good=${item.expiring}></ak-status-label>`,
|
html`<ak-status-label type="warning" ?good=${item.expiring}></ak-status-label>`,
|
||||||
html`${item.expiring ? item.expires?.toLocaleString() : msg("-")}`,
|
html`${item.expires
|
||||||
|
? html`<div>${getRelativeTime(item.expires)}</div>
|
||||||
|
<small>${item.expires.toLocaleString()}</small>`
|
||||||
|
: msg("-")}`,
|
||||||
html`${intentToLabel(item.intent ?? IntentEnum.Api)}`,
|
html`${intentToLabel(item.intent ?? IntentEnum.Api)}`,
|
||||||
html`
|
html`
|
||||||
${!item.managed
|
${!item.managed
|
||||||
|
79
web/src/admin/users/UserApplicationTable.ts
Normal file
79
web/src/admin/users/UserApplicationTable.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { applicationListStyle } from "@goauthentik/app/admin/applications/ApplicationListPage";
|
||||||
|
import { DEFAULT_CONFIG } from "@goauthentik/app/common/api/config";
|
||||||
|
import { uiConfig } from "@goauthentik/app/common/ui/config";
|
||||||
|
import { PFSize } from "@goauthentik/app/elements/Spinner";
|
||||||
|
import { PaginatedResponse, Table, TableColumn } from "@goauthentik/app/elements/table/Table";
|
||||||
|
import "@goauthentik/components/ak-app-icon";
|
||||||
|
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||||
|
|
||||||
|
import { msg } from "@lit/localize";
|
||||||
|
import { CSSResult, TemplateResult, html } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
|
||||||
|
import { Application, CoreApi, User } from "@goauthentik/api";
|
||||||
|
|
||||||
|
@customElement("ak-user-application-table")
|
||||||
|
export class UserApplicationTable extends Table<Application> {
|
||||||
|
@property({ attribute: false })
|
||||||
|
user?: User;
|
||||||
|
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return super.styles.concat(applicationListStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
async apiEndpoint(page: number): Promise<PaginatedResponse<Application>> {
|
||||||
|
return new CoreApi(DEFAULT_CONFIG).coreApplicationsList({
|
||||||
|
forUser: this.user?.pk,
|
||||||
|
page: page,
|
||||||
|
pageSize: (await uiConfig()).pagination.perPage,
|
||||||
|
ordering: this.order,
|
||||||
|
search: this.search || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
columns(): TableColumn[] {
|
||||||
|
return [
|
||||||
|
new TableColumn(""),
|
||||||
|
new TableColumn(msg("Name"), "name"),
|
||||||
|
new TableColumn(msg("Group"), "group"),
|
||||||
|
new TableColumn(msg("Provider")),
|
||||||
|
new TableColumn(msg("Provider Type")),
|
||||||
|
new TableColumn(msg("Actions")),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
row(item: Application): TemplateResult[] {
|
||||||
|
return [
|
||||||
|
html`<ak-app-icon size=${PFSize.Medium} .app=${item}></ak-app-icon>`,
|
||||||
|
html`<a href="#/core/applications/${item.slug}">
|
||||||
|
<div>${item.name}</div>
|
||||||
|
${item.metaPublisher ? html`<small>${item.metaPublisher}</small>` : html``}
|
||||||
|
</a>`,
|
||||||
|
html`${item.group || msg("-")}`,
|
||||||
|
item.provider
|
||||||
|
? html`<a href="#/core/providers/${item.providerObj?.pk}">
|
||||||
|
${item.providerObj?.name}
|
||||||
|
</a>`
|
||||||
|
: html`-`,
|
||||||
|
html`${item.providerObj?.verboseName || msg("-")}`,
|
||||||
|
html`<ak-forms-modal>
|
||||||
|
<span slot="submit"> ${msg("Update")} </span>
|
||||||
|
<span slot="header"> ${msg("Update Application")} </span>
|
||||||
|
<ak-application-form slot="form" .instancePk=${item.slug}>
|
||||||
|
</ak-application-form>
|
||||||
|
<button slot="trigger" class="pf-c-button pf-m-plain">
|
||||||
|
<pf-tooltip position="top" content=${msg("Edit")}>
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</pf-tooltip>
|
||||||
|
</button>
|
||||||
|
</ak-forms-modal>
|
||||||
|
${item.launchUrl
|
||||||
|
? html`<a href=${item.launchUrl} target="_blank" class="pf-c-button pf-m-plain">
|
||||||
|
<pf-tooltip position="top" content=${msg("Open")}>
|
||||||
|
<i class="fas fa-share-square"></i>
|
||||||
|
</pf-tooltip>
|
||||||
|
</a>`
|
||||||
|
: html``}`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -9,7 +9,7 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
|||||||
import { userTypeToLabel } from "@goauthentik/common/labels";
|
import { userTypeToLabel } from "@goauthentik/common/labels";
|
||||||
import { MessageLevel } from "@goauthentik/common/messages";
|
import { MessageLevel } from "@goauthentik/common/messages";
|
||||||
import { DefaultUIConfig, uiConfig } from "@goauthentik/common/ui/config";
|
import { DefaultUIConfig, uiConfig } from "@goauthentik/common/ui/config";
|
||||||
import { first } from "@goauthentik/common/utils";
|
import { getRelativeTime } from "@goauthentik/common/utils";
|
||||||
import "@goauthentik/components/ak-status-label";
|
import "@goauthentik/components/ak-status-label";
|
||||||
import { rootInterface } from "@goauthentik/elements/Base";
|
import { rootInterface } from "@goauthentik/elements/Base";
|
||||||
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
|
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
|
||||||
@ -159,6 +159,7 @@ export class UserListPage extends WithBrandConfig(WithCapabilitiesConfig(TablePa
|
|||||||
new TableColumn(msg("Name"), "username"),
|
new TableColumn(msg("Name"), "username"),
|
||||||
new TableColumn(msg("Active"), "is_active"),
|
new TableColumn(msg("Active"), "is_active"),
|
||||||
new TableColumn(msg("Last login"), "last_login"),
|
new TableColumn(msg("Last login"), "last_login"),
|
||||||
|
new TableColumn(msg("Type"), "type"),
|
||||||
new TableColumn(msg("Actions")),
|
new TableColumn(msg("Actions")),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -246,11 +247,15 @@ export class UserListPage extends WithBrandConfig(WithCapabilitiesConfig(TablePa
|
|||||||
this.can(CapabilitiesEnum.CanImpersonate) && item.pk !== this.me?.user.pk;
|
this.can(CapabilitiesEnum.CanImpersonate) && item.pk !== this.me?.user.pk;
|
||||||
return [
|
return [
|
||||||
html`<a href="#/identity/users/${item.pk}">
|
html`<a href="#/identity/users/${item.pk}">
|
||||||
<div>${item.username}</div>
|
<div>${item.username}</div>
|
||||||
<small>${item.name === "" ? msg("<No name set>") : item.name}</small> </a
|
<small>${item.name === "" ? msg("<No name set>") : item.name}</small>
|
||||||
> <small>${userTypeToLabel(item.type)}</small>`,
|
</a>`,
|
||||||
html`<ak-status-label ?good=${item.isActive}></ak-status-label>`,
|
html`<ak-status-label ?good=${item.isActive}></ak-status-label>`,
|
||||||
html`${first(item.lastLogin?.toLocaleString(), msg("-"))}`,
|
html`${item.lastLogin
|
||||||
|
? html`<div>${getRelativeTime(item.lastLogin)}</div>
|
||||||
|
<small>${item.lastLogin.toLocaleString()}</small>`
|
||||||
|
: msg("-")}`,
|
||||||
|
html`${userTypeToLabel(item.type)}`,
|
||||||
html`<ak-forms-modal>
|
html`<ak-forms-modal>
|
||||||
<span slot="submit"> ${msg("Update")} </span>
|
<span slot="submit"> ${msg("Update")} </span>
|
||||||
<span slot="header"> ${msg("Update User")} </span>
|
<span slot="header"> ${msg("Update User")} </span>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import "@goauthentik/admin/groups/RelatedGroupList";
|
import "@goauthentik/admin/groups/RelatedGroupList";
|
||||||
import "@goauthentik/admin/users/UserActiveForm";
|
import "@goauthentik/admin/users/UserActiveForm";
|
||||||
|
import "@goauthentik/admin/users/UserApplicationTable";
|
||||||
import "@goauthentik/admin/users/UserChart";
|
import "@goauthentik/admin/users/UserChart";
|
||||||
import "@goauthentik/admin/users/UserForm";
|
import "@goauthentik/admin/users/UserForm";
|
||||||
import "@goauthentik/admin/users/UserPasswordForm";
|
import "@goauthentik/admin/users/UserPasswordForm";
|
||||||
@ -8,6 +9,7 @@ import {
|
|||||||
requestRecoveryLink,
|
requestRecoveryLink,
|
||||||
} from "@goauthentik/app/admin/users/UserListPage";
|
} from "@goauthentik/app/admin/users/UserListPage";
|
||||||
import { me } from "@goauthentik/app/common/users";
|
import { me } from "@goauthentik/app/common/users";
|
||||||
|
import { getRelativeTime } from "@goauthentik/app/common/utils";
|
||||||
import "@goauthentik/app/elements/oauth/UserAccessTokenList";
|
import "@goauthentik/app/elements/oauth/UserAccessTokenList";
|
||||||
import "@goauthentik/app/elements/oauth/UserRefreshTokenList";
|
import "@goauthentik/app/elements/oauth/UserRefreshTokenList";
|
||||||
import "@goauthentik/app/elements/rbac/ObjectPermissionsPage";
|
import "@goauthentik/app/elements/rbac/ObjectPermissionsPage";
|
||||||
@ -147,7 +149,10 @@ export class UserViewPage extends WithCapabilitiesConfig(AKElement) {
|
|||||||
[msg("Username"), user.username],
|
[msg("Username"), user.username],
|
||||||
[msg("Name"), user.name],
|
[msg("Name"), user.name],
|
||||||
[msg("Email"), user.email || "-"],
|
[msg("Email"), user.email || "-"],
|
||||||
[msg("Last login"), user.lastLogin?.toLocaleString()],
|
[msg("Last login"), user.lastLogin
|
||||||
|
? html`<div>${getRelativeTime(user.lastLogin)}</div>
|
||||||
|
<small>${user.lastLogin.toLocaleString()}</small>`
|
||||||
|
: html`${msg("-")}`],
|
||||||
[msg("Active"), html`<ak-status-label type="warning" ?good=${user.isActive}></ak-status-label>`],
|
[msg("Active"), html`<ak-status-label type="warning" ?good=${user.isActive}></ak-status-label>`],
|
||||||
[msg("Type"), userTypeToLabel(user.type)],
|
[msg("Type"), userTypeToLabel(user.type)],
|
||||||
[msg("Superuser"), html`<ak-status-label type="warning" ?good=${user.isSuperuser}></ak-status-label>`],
|
[msg("Superuser"), html`<ak-status-label type="warning" ?good=${user.isSuperuser}></ak-status-label>`],
|
||||||
@ -317,6 +322,14 @@ export class UserViewPage extends WithCapabilitiesConfig(AKElement) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderTabApplications(user: User): TemplateResult {
|
||||||
|
return html`<div class="pf-c-card">
|
||||||
|
<div class="pf-c-card__body">
|
||||||
|
<ak-user-application-table .user=${user}></ak-user-application-table>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
renderBody() {
|
renderBody() {
|
||||||
if (!this.user) {
|
if (!this.user) {
|
||||||
return nothing;
|
return nothing;
|
||||||
@ -399,6 +412,13 @@ export class UserViewPage extends WithCapabilitiesConfig(AKElement) {
|
|||||||
<section slot="page-credentials" data-tab-title="${msg("Credentials / Tokens")}">
|
<section slot="page-credentials" data-tab-title="${msg("Credentials / Tokens")}">
|
||||||
${this.renderTabCredentialsToken(this.user)}
|
${this.renderTabCredentialsToken(this.user)}
|
||||||
</section>
|
</section>
|
||||||
|
<section
|
||||||
|
slot="page-applications"
|
||||||
|
data-tab-title="${msg("Applications")}"
|
||||||
|
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||||
|
>
|
||||||
|
${this.renderTabApplications(this.user)}
|
||||||
|
</section>
|
||||||
<ak-rbac-object-permission-page
|
<ak-rbac-object-permission-page
|
||||||
slot="page-permissions"
|
slot="page-permissions"
|
||||||
data-tab-title="${msg("Permissions")}"
|
data-tab-title="${msg("Permissions")}"
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { getRelativeTime } from "@goauthentik/app/common/utils";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { EVENT_NOTIFICATION_DRAWER_TOGGLE, EVENT_REFRESH } from "@goauthentik/common/constants";
|
import { EVENT_NOTIFICATION_DRAWER_TOGGLE, EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||||
import { actionToLabel } from "@goauthentik/common/labels";
|
import { actionToLabel } from "@goauthentik/common/labels";
|
||||||
@ -6,6 +7,7 @@ import { me } from "@goauthentik/common/users";
|
|||||||
import { AKElement } from "@goauthentik/elements/Base";
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
|
import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
|
||||||
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||||
|
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||||
|
|
||||||
import { msg, str } from "@lit/localize";
|
import { msg, str } from "@lit/localize";
|
||||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||||
@ -132,7 +134,9 @@ export class NotificationDrawer extends AKElement {
|
|||||||
</div>
|
</div>
|
||||||
<p class="pf-c-notification-drawer__list-item-description">${item.body}</p>
|
<p class="pf-c-notification-drawer__list-item-description">${item.body}</p>
|
||||||
<small class="pf-c-notification-drawer__list-item-timestamp"
|
<small class="pf-c-notification-drawer__list-item-timestamp"
|
||||||
>${item.created?.toLocaleString()}</small
|
><pf-tooltip position="top" .content=${item.created?.toLocaleString()}>
|
||||||
|
${getRelativeTime(item.created!)}
|
||||||
|
</pf-tooltip></small
|
||||||
>
|
>
|
||||||
</li>`;
|
</li>`;
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { getRelativeTime } from "@goauthentik/app/common/utils";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { uiConfig } from "@goauthentik/common/ui/config";
|
import { uiConfig } from "@goauthentik/common/ui/config";
|
||||||
import "@goauthentik/components/ak-status-label";
|
import "@goauthentik/components/ak-status-label";
|
||||||
@ -86,7 +87,10 @@ export class UserOAuthAccessTokenList extends Table<TokenModel> {
|
|||||||
return [
|
return [
|
||||||
html`<a href="#/core/providers/${item.provider?.pk}"> ${item.provider?.name} </a>`,
|
html`<a href="#/core/providers/${item.provider?.pk}"> ${item.provider?.name} </a>`,
|
||||||
html`<ak-status-label type="warning" ?good=${item.revoked}></ak-status-label>`,
|
html`<ak-status-label type="warning" ?good=${item.revoked}></ak-status-label>`,
|
||||||
html`${item.expires?.toLocaleString()}`,
|
html`${item.expires
|
||||||
|
? html`<div>${getRelativeTime(item.expires)}</div>
|
||||||
|
<small>${item.expires.toLocaleString()}</small>`
|
||||||
|
: msg("-")}`,
|
||||||
html`${item.scope.join(", ")}`,
|
html`${item.scope.join(", ")}`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { getRelativeTime } from "@goauthentik/app/common/utils";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { uiConfig } from "@goauthentik/common/ui/config";
|
import { uiConfig } from "@goauthentik/common/ui/config";
|
||||||
import "@goauthentik/components/ak-status-label";
|
import "@goauthentik/components/ak-status-label";
|
||||||
@ -87,7 +88,10 @@ export class UserOAuthRefreshTokenList extends Table<TokenModel> {
|
|||||||
return [
|
return [
|
||||||
html`<a href="#/core/providers/${item.provider?.pk}"> ${item.provider?.name} </a>`,
|
html`<a href="#/core/providers/${item.provider?.pk}"> ${item.provider?.name} </a>`,
|
||||||
html`<ak-status-label type="warning" ?good=${item.revoked}></ak-status-label>`,
|
html`<ak-status-label type="warning" ?good=${item.revoked}></ak-status-label>`,
|
||||||
html`${item.expires?.toLocaleString()}`,
|
html`${item.expires
|
||||||
|
? html`<div>${getRelativeTime(item.expires)}</div>
|
||||||
|
<small>${item.expires.toLocaleString()}</small>`
|
||||||
|
: msg("-")}`,
|
||||||
html`${item.scope.join(", ")}`,
|
html`${item.scope.join(", ")}`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { getRelativeTime } from "@goauthentik/app/common/utils";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { uiConfig } from "@goauthentik/common/ui/config";
|
import { uiConfig } from "@goauthentik/common/ui/config";
|
||||||
import "@goauthentik/elements/forms/DeleteBulkForm";
|
import "@goauthentik/elements/forms/DeleteBulkForm";
|
||||||
@ -61,7 +62,10 @@ export class UserConsentList extends Table<UserConsent> {
|
|||||||
row(item: UserConsent): TemplateResult[] {
|
row(item: UserConsent): TemplateResult[] {
|
||||||
return [
|
return [
|
||||||
html`${item.application.name}`,
|
html`${item.application.name}`,
|
||||||
html`${item.expires?.toLocaleString()}`,
|
html`${item.expires
|
||||||
|
? html`<div>${getRelativeTime(item.expires)}</div>
|
||||||
|
<small>${item.expires.toLocaleString()}</small>`
|
||||||
|
: msg("-")}`,
|
||||||
html`${item.permissions || "-"}`,
|
html`${item.permissions || "-"}`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { getRelativeTime } from "@goauthentik/app/common/utils";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { intentToLabel } from "@goauthentik/common/labels";
|
import { intentToLabel } from "@goauthentik/common/labels";
|
||||||
import { uiConfig } from "@goauthentik/common/ui/config";
|
import { uiConfig } from "@goauthentik/common/ui/config";
|
||||||
@ -108,7 +109,14 @@ export class UserTokenList extends Table<Token> {
|
|||||||
</dt>
|
</dt>
|
||||||
<dd class="pf-c-description-list__description">
|
<dd class="pf-c-description-list__description">
|
||||||
<div class="pf-c-description-list__text">
|
<div class="pf-c-description-list__text">
|
||||||
${item.expiring ? item.expires?.toLocaleString() : msg("-")}
|
${item.expiring
|
||||||
|
? html`<pf-tooltip
|
||||||
|
position="top"
|
||||||
|
.content=${item.expires?.toLocaleString()}
|
||||||
|
>
|
||||||
|
${getRelativeTime(item.expires!)}
|
||||||
|
</pf-tooltip>`
|
||||||
|
: msg("-")}
|
||||||
</div>
|
</div>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
Reference in New Issue
Block a user