web: Table parity (#427)
* core: fix application API always being sorted by name * web: add sorting to tables * web: add search to TablePage * core: add search to applications API * core: add MetaNameSerializer * *: fix signature for non-modal serializers * providers/*: implement MetaNameSerializer * web: implement full app list page, use as default in sidebar * web: fix linting errors * admin: remove old application list * web: fix default sorting for application list * web: fix spacing for search element in toolbar
This commit is contained in:
parent
c3e9168b46
commit
79da2bf698
|
@ -4,10 +4,9 @@ from collections import Counter
|
|||
from datetime import timedelta
|
||||
from typing import Dict, List
|
||||
|
||||
from django.db.models import Count, ExpressionWrapper, F
|
||||
from django.db.models import Count, ExpressionWrapper, F, Model
|
||||
from django.db.models.fields import DurationField
|
||||
from django.db.models.functions import ExtractHour
|
||||
from django.http import response
|
||||
from django.utils.timezone import now
|
||||
from drf_yasg2.utils import swagger_auto_schema
|
||||
from rest_framework.fields import SerializerMethodField
|
||||
|
@ -60,10 +59,10 @@ class AdministrationMetricsSerializer(Serializer):
|
|||
"""Get failed logins per hour for the last 24 hours"""
|
||||
return get_events_per_1h(action=EventAction.LOGIN_FAILED)
|
||||
|
||||
def create(self, request: Request) -> response:
|
||||
def create(self, validated_data: dict) -> Model:
|
||||
raise NotImplementedError
|
||||
|
||||
def update(self, request: Request) -> Response:
|
||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
from importlib import import_module
|
||||
|
||||
from django.contrib import messages
|
||||
from django.db.models import Model
|
||||
from django.http.response import Http404
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from drf_yasg2.utils import swagger_auto_schema
|
||||
|
@ -26,10 +27,10 @@ class TaskSerializer(Serializer):
|
|||
status = IntegerField(source="result.status.value")
|
||||
messages = ListField(source="result.messages")
|
||||
|
||||
def create(self, request: Request) -> Response:
|
||||
def create(self, validated_data: dict) -> Model:
|
||||
raise NotImplementedError
|
||||
|
||||
def update(self, request: Request) -> Response:
|
||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
"""authentik administration overview"""
|
||||
from django.core.cache import cache
|
||||
from django.db.models import Model
|
||||
from drf_yasg2.utils import swagger_auto_schema
|
||||
from packaging.version import parse
|
||||
from rest_framework.fields import SerializerMethodField
|
||||
|
@ -39,10 +40,10 @@ class VersionSerializer(Serializer):
|
|||
self.get_version_latest(instance)
|
||||
)
|
||||
|
||||
def create(self, request: Request) -> Response:
|
||||
def create(self, validated_data: dict) -> Model:
|
||||
raise NotImplementedError
|
||||
|
||||
def update(self, request: Request) -> Response:
|
||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
|
|
|
@ -1,131 +0,0 @@
|
|||
{% extends "administration/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load authentik_utils %}
|
||||
|
||||
{% block content %}
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
<div class="pf-c-content">
|
||||
<h1>
|
||||
<i class="pf-icon pf-icon-applications"></i>
|
||||
{% trans 'Applications' %}
|
||||
</h1>
|
||||
<p>{% trans "External Applications which use authentik as Identity-Provider, utilizing protocols like OAuth2 and SAML." %}</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-c-card">
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<ak-modal-button href="{% url 'authentik_admin:application-create' %}">
|
||||
<ak-spinner-button slot="trigger" class="pf-m-primary">
|
||||
{% trans 'Create' %}
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
<button role="ak-refresh" class="pf-c-button pf-m-primary">
|
||||
{% trans 'Refresh' %}
|
||||
</button>
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
<tr role="row">
|
||||
<th role="columnheader"></th>
|
||||
<th role="columnheader" scope="col">{% trans 'Name' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Slug' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Provider' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Provider Type' %}</th>
|
||||
<th role="columnheader"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody role="rowgroup">
|
||||
{% for application in object_list %}
|
||||
<tr role="row">
|
||||
<td role="cell" {% if application.meta_icon %} style="vertical-align: bottom;" {% endif %}>
|
||||
{% if application.meta_icon %}
|
||||
<img class="app-icon pf-c-avatar" src="{{ application.meta_icon.url }}" alt="{% trans 'Application Icon' %}">
|
||||
{% else %}
|
||||
<i class="pf-icon pf-icon-arrow"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td role="cell">
|
||||
<a href="/applications/{{ application.slug }}/">
|
||||
<div>
|
||||
{{ application.name }}
|
||||
</div>
|
||||
{% if application.meta_publisher %}
|
||||
<small>{{ application.meta_publisher }}</small>
|
||||
{% endif %}
|
||||
</a>
|
||||
</td>
|
||||
<td role="cell">
|
||||
<code>{{ application.slug }}</span>
|
||||
</td>
|
||||
<td role="cell">
|
||||
<span>
|
||||
{{ application.get_provider }}
|
||||
</span>
|
||||
</td>
|
||||
<td role="cell">
|
||||
<span>
|
||||
{{ application.get_provider|verbose_name }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<ak-modal-button href="{% url 'authentik_admin:application-update' pk=application.pk %}">
|
||||
<ak-spinner-button slot="trigger" class="pf-m-secondary">
|
||||
{% trans 'Edit' %}
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
<ak-modal-button href="{% url 'authentik_admin:application-delete' pk=application.pk %}">
|
||||
<ak-spinner-button slot="trigger" class="pf-m-danger">
|
||||
{% trans 'Delete' %}
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pf-c-pagination pf-m-bottom">
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
{% include 'partials/toolbar_search.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-empty-state">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="pf-icon pf-icon-applications pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Applications.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% if request.GET.search != "" %}
|
||||
{% trans "Your search query doesn't match any application." %}
|
||||
{% else %}
|
||||
{% trans 'Currently no applications exist. Click the button below to create one.' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<ak-modal-button href="{% url 'authentik_admin:application-create' %}">
|
||||
<ak-spinner-button slot="trigger" class="pf-m-primary">
|
||||
{% trans 'Create' %}
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -35,9 +35,6 @@ urlpatterns = [
|
|||
name="overview-clear-policy-cache",
|
||||
),
|
||||
# Applications
|
||||
path(
|
||||
"applications/", applications.ApplicationListView.as_view(), name="applications"
|
||||
),
|
||||
path(
|
||||
"applications/create/",
|
||||
applications.ApplicationCreateView.as_view(),
|
||||
|
|
|
@ -6,44 +6,18 @@ from django.contrib.auth.mixins import (
|
|||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import ListView, UpdateView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
from django.views.generic import UpdateView
|
||||
from guardian.mixins import PermissionRequiredMixin
|
||||
|
||||
from authentik.admin.views.utils import (
|
||||
BackSuccessUrlMixin,
|
||||
DeleteMessageView,
|
||||
SearchListMixin,
|
||||
UserPaginateListMixin,
|
||||
)
|
||||
from authentik.core.forms.applications import ApplicationForm
|
||||
from authentik.core.models import Application
|
||||
from authentik.lib.views import CreateAssignPermView
|
||||
|
||||
|
||||
class ApplicationListView(
|
||||
LoginRequiredMixin,
|
||||
PermissionListMixin,
|
||||
UserPaginateListMixin,
|
||||
SearchListMixin,
|
||||
ListView,
|
||||
):
|
||||
"""Show list of all applications"""
|
||||
|
||||
model = Application
|
||||
permission_required = "authentik_core.view_application"
|
||||
ordering = "name"
|
||||
template_name = "administration/application/list.html"
|
||||
|
||||
search_fields = [
|
||||
"name",
|
||||
"slug",
|
||||
"meta_launch_url",
|
||||
"meta_icon_url",
|
||||
"meta_description",
|
||||
"meta_publisher",
|
||||
]
|
||||
|
||||
|
||||
class ApplicationCreateView(
|
||||
SuccessMessageMixin,
|
||||
BackSuccessUrlMixin,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""core Configs API"""
|
||||
from django.db.models import Model
|
||||
from drf_yasg2.utils import swagger_auto_schema
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.request import Request
|
||||
|
@ -19,10 +20,10 @@ class ConfigSerializer(Serializer):
|
|||
error_reporting_environment = ReadOnlyField()
|
||||
error_reporting_send_pii = ReadOnlyField()
|
||||
|
||||
def create(self, request: Request) -> Response:
|
||||
def create(self, validated_data: dict) -> Model:
|
||||
raise NotImplementedError
|
||||
|
||||
def update(self, request: Request) -> Response:
|
||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
"""core messages API"""
|
||||
from django.contrib.messages import get_messages
|
||||
from django.db.models import Model
|
||||
from drf_yasg2.utils import swagger_auto_schema
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.request import Request
|
||||
|
@ -17,10 +18,10 @@ class MessageSerializer(Serializer):
|
|||
extra_tags = ReadOnlyField()
|
||||
level_tag = ReadOnlyField()
|
||||
|
||||
def create(self, request: Request) -> Response:
|
||||
def create(self, validated_data: dict) -> Model:
|
||||
raise NotImplementedError
|
||||
|
||||
def update(self, request: Request) -> Response:
|
||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ from rest_framework.viewsets import ModelViewSet
|
|||
from rest_framework_guardian.filters import ObjectPermissionsFilter
|
||||
|
||||
from authentik.admin.api.metrics import get_events_per_1h
|
||||
from authentik.core.api.providers import ProviderSerializer
|
||||
from authentik.core.models import Application
|
||||
from authentik.events.models import EventAction
|
||||
from authentik.policies.engine import PolicyEngine
|
||||
|
@ -21,6 +22,7 @@ class ApplicationSerializer(ModelSerializer):
|
|||
"""Application Serializer"""
|
||||
|
||||
launch_url = SerializerMethodField()
|
||||
provider = ProviderSerializer(source="get_provider")
|
||||
|
||||
def get_launch_url(self, instance: Application) -> str:
|
||||
"""Get generated launch URL"""
|
||||
|
@ -48,7 +50,15 @@ class ApplicationViewSet(ModelViewSet):
|
|||
|
||||
queryset = Application.objects.all()
|
||||
serializer_class = ApplicationSerializer
|
||||
search_fields = [
|
||||
"name",
|
||||
"slug",
|
||||
"meta_launch_url",
|
||||
"meta_description",
|
||||
"meta_publisher",
|
||||
]
|
||||
lookup_field = "slug"
|
||||
ordering = ["name"]
|
||||
|
||||
def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet:
|
||||
"""Custom filter_queryset method which ignores guardian, but still supports sorting"""
|
||||
|
@ -63,7 +73,7 @@ class ApplicationViewSet(ModelViewSet):
|
|||
queryset = self._filter_queryset_for_list(self.get_queryset())
|
||||
self.paginate_queryset(queryset)
|
||||
allowed_applications = []
|
||||
for application in queryset.order_by("name"):
|
||||
for application in queryset:
|
||||
engine = PolicyEngine(application, self.request.user, self.request)
|
||||
engine.build()
|
||||
if engine.passing:
|
||||
|
|
|
@ -2,15 +2,16 @@
|
|||
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.utils import MetaNameSerializer
|
||||
from authentik.core.models import Provider
|
||||
|
||||
|
||||
class ProviderSerializer(ModelSerializer):
|
||||
class ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
||||
"""Provider Serializer"""
|
||||
|
||||
__type__ = SerializerMethodField(method_name="get_type")
|
||||
object_type = SerializerMethodField()
|
||||
|
||||
def get_type(self, obj):
|
||||
def get_object_type(self, obj):
|
||||
"""Get object type so that we know which API Endpoint to use to get the full object"""
|
||||
return obj._meta.object_name.lower().replace("provider", "")
|
||||
|
||||
|
@ -29,7 +30,9 @@ class ProviderSerializer(ModelSerializer):
|
|||
"application",
|
||||
"authorization_flow",
|
||||
"property_mappings",
|
||||
"__type__",
|
||||
"object_type",
|
||||
"verbose_name",
|
||||
"verbose_name_plural",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
"""API Utilities"""
|
||||
from django.db.models import Model
|
||||
from rest_framework.serializers import Serializer, SerializerMethodField
|
||||
|
||||
|
||||
class MetaNameSerializer(Serializer):
|
||||
"""Add verbose names to response"""
|
||||
|
||||
verbose_name = SerializerMethodField()
|
||||
verbose_name_plural = SerializerMethodField()
|
||||
|
||||
def create(self, validated_data: dict) -> Model:
|
||||
raise NotImplementedError
|
||||
|
||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
||||
raise NotImplementedError
|
||||
|
||||
def get_verbose_name(self, obj: Model) -> str:
|
||||
"""Return object's verbose_name"""
|
||||
return obj._meta.verbose_name
|
||||
|
||||
def get_verbose_name_plural(self, obj: Model) -> str:
|
||||
"""Return object's plural verbose_name"""
|
||||
return obj._meta.verbose_name_plural
|
|
@ -2,10 +2,11 @@
|
|||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.utils import MetaNameSerializer
|
||||
from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping
|
||||
|
||||
|
||||
class OAuth2ProviderSerializer(ModelSerializer):
|
||||
class OAuth2ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
||||
"""OAuth2Provider Serializer"""
|
||||
|
||||
class Meta:
|
||||
|
@ -25,6 +26,8 @@ class OAuth2ProviderSerializer(ModelSerializer):
|
|||
"redirect_uris",
|
||||
"sub_mode",
|
||||
"property_mappings",
|
||||
"verbose_name",
|
||||
"verbose_name_plural",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ from rest_framework.response import Response
|
|||
from rest_framework.serializers import ModelSerializer, Serializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.utils import MetaNameSerializer
|
||||
from authentik.providers.oauth2.views.provider import ProviderInfoView
|
||||
from authentik.providers.proxy.models import ProxyProvider
|
||||
|
||||
|
@ -33,7 +34,7 @@ class OpenIDConnectConfigurationSerializer(Serializer):
|
|||
raise NotImplementedError
|
||||
|
||||
|
||||
class ProxyProviderSerializer(ModelSerializer):
|
||||
class ProxyProviderSerializer(MetaNameSerializer, ModelSerializer):
|
||||
"""ProxyProvider Serializer"""
|
||||
|
||||
def create(self, validated_data):
|
||||
|
@ -60,6 +61,8 @@ class ProxyProviderSerializer(ModelSerializer):
|
|||
"basic_auth_enabled",
|
||||
"basic_auth_password_attribute",
|
||||
"basic_auth_user_attribute",
|
||||
"verbose_name",
|
||||
"verbose_name_plural",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -2,10 +2,11 @@
|
|||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.utils import MetaNameSerializer
|
||||
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
|
||||
|
||||
|
||||
class SAMLProviderSerializer(ModelSerializer):
|
||||
class SAMLProviderSerializer(ModelSerializer, MetaNameSerializer):
|
||||
"""SAMLProvider Serializer"""
|
||||
|
||||
class Meta:
|
||||
|
@ -25,6 +26,8 @@ class SAMLProviderSerializer(ModelSerializer):
|
|||
"signature_algorithm",
|
||||
"signing_kp",
|
||||
"verification_kp",
|
||||
"verbose_name",
|
||||
"verbose_name_plural",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -148,9 +148,9 @@ class SAMLProvider(Provider):
|
|||
|
||||
@property
|
||||
def serializer(self) -> Type[Serializer]:
|
||||
from authentik.providers.saml.api import SAMLPropertyMappingSerializer
|
||||
from authentik.providers.saml.api import SAMLProviderSerializer
|
||||
|
||||
return SAMLPropertyMappingSerializer
|
||||
return SAMLProviderSerializer
|
||||
|
||||
@property
|
||||
def form(self) -> Type[ModelForm]:
|
||||
|
|
106
swagger.yaml
106
swagger.yaml
|
@ -6729,11 +6729,55 @@ definitions:
|
|||
title: Outdated
|
||||
type: boolean
|
||||
readOnly: true
|
||||
Provider:
|
||||
title: Provider
|
||||
description: Provider Serializer
|
||||
required:
|
||||
- name
|
||||
- application
|
||||
- authorization_flow
|
||||
type: object
|
||||
properties:
|
||||
pk:
|
||||
title: ID
|
||||
type: integer
|
||||
readOnly: true
|
||||
name:
|
||||
title: Name
|
||||
type: string
|
||||
minLength: 1
|
||||
application:
|
||||
title: Application
|
||||
type: string
|
||||
authorization_flow:
|
||||
title: Authorization flow
|
||||
description: Flow used when authorizing this provider.
|
||||
type: string
|
||||
format: uuid
|
||||
property_mappings:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
uniqueItems: true
|
||||
object_type:
|
||||
title: Object type
|
||||
type: string
|
||||
readOnly: true
|
||||
verbose_name:
|
||||
title: Verbose name
|
||||
type: string
|
||||
readOnly: true
|
||||
verbose_name_plural:
|
||||
title: Verbose name plural
|
||||
type: string
|
||||
readOnly: true
|
||||
Application:
|
||||
description: Application Serializer
|
||||
required:
|
||||
- name
|
||||
- slug
|
||||
- provider
|
||||
type: object
|
||||
properties:
|
||||
pk:
|
||||
|
@ -6755,9 +6799,7 @@ definitions:
|
|||
maxLength: 50
|
||||
minLength: 1
|
||||
provider:
|
||||
title: Provider
|
||||
type: integer
|
||||
x-nullable: true
|
||||
$ref: '#/definitions/Provider'
|
||||
launch_url:
|
||||
title: Launch url
|
||||
type: string
|
||||
|
@ -7720,40 +7762,6 @@ definitions:
|
|||
title: Expression
|
||||
type: string
|
||||
minLength: 1
|
||||
Provider:
|
||||
description: Provider Serializer
|
||||
required:
|
||||
- name
|
||||
- application
|
||||
- authorization_flow
|
||||
type: object
|
||||
properties:
|
||||
pk:
|
||||
title: ID
|
||||
type: integer
|
||||
readOnly: true
|
||||
name:
|
||||
title: Name
|
||||
type: string
|
||||
minLength: 1
|
||||
application:
|
||||
title: Application
|
||||
type: string
|
||||
authorization_flow:
|
||||
title: Authorization flow
|
||||
description: Flow used when authorizing this provider.
|
||||
type: string
|
||||
format: uuid
|
||||
property_mappings:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
uniqueItems: true
|
||||
__type__:
|
||||
title: 'type '
|
||||
type: string
|
||||
readOnly: true
|
||||
OAuth2Provider:
|
||||
description: OAuth2Provider Serializer
|
||||
required:
|
||||
|
@ -7845,6 +7853,14 @@ definitions:
|
|||
type: string
|
||||
format: uuid
|
||||
uniqueItems: true
|
||||
verbose_name:
|
||||
title: Verbose name
|
||||
type: string
|
||||
readOnly: true
|
||||
verbose_name_plural:
|
||||
title: Verbose name plural
|
||||
type: string
|
||||
readOnly: true
|
||||
ProxyProvider:
|
||||
description: ProxyProvider Serializer
|
||||
required:
|
||||
|
@ -7898,6 +7914,14 @@ definitions:
|
|||
description: User/Group Attribute used for the user part of the HTTP-Basic
|
||||
Header. If not set, the user's Email address is used.
|
||||
type: string
|
||||
verbose_name:
|
||||
title: Verbose name
|
||||
type: string
|
||||
readOnly: true
|
||||
verbose_name_plural:
|
||||
title: Verbose name plural
|
||||
type: string
|
||||
readOnly: true
|
||||
SAMLProvider:
|
||||
description: SAMLProvider Serializer
|
||||
required:
|
||||
|
@ -7984,6 +8008,14 @@ definitions:
|
|||
type: string
|
||||
format: uuid
|
||||
x-nullable: true
|
||||
verbose_name:
|
||||
title: Verbose name
|
||||
type: string
|
||||
readOnly: true
|
||||
verbose_name_plural:
|
||||
title: Verbose name plural
|
||||
type: string
|
||||
readOnly: true
|
||||
Config:
|
||||
description: Serialize authentik Config into DRF Object
|
||||
type: object
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { DefaultClient, PBResponse, QueryArguments } from "./Client";
|
||||
import { Provider } from "./Providers";
|
||||
|
||||
export class Application {
|
||||
pk: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
provider: number;
|
||||
provider: Provider;
|
||||
|
||||
launch_url: string;
|
||||
meta_launch_url: string;
|
||||
|
@ -24,4 +25,8 @@ export class Application {
|
|||
static list(filter?: QueryArguments): Promise<PBResponse<Application>> {
|
||||
return DefaultClient.fetch<PBResponse<Application>>(["core", "applications"], filter);
|
||||
}
|
||||
|
||||
static adminUrl(rest: string): string {
|
||||
return `/administration/applications/${rest}`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ export class Provider {
|
|||
pk: number;
|
||||
name: string;
|
||||
authorization_flow: string;
|
||||
verbose_name: string;
|
||||
verbose_name_plural: string;
|
||||
|
||||
constructor() {
|
||||
throw Error();
|
||||
|
|
|
@ -136,6 +136,12 @@ select[multiple] {
|
|||
--pf-c-table--BorderColor: var(--ak-dark-background-lighter);
|
||||
--pf-c-table--cell--Color: var(--ak-dark-foreground);
|
||||
}
|
||||
.pf-c-table__text {
|
||||
color: var(--ak-dark-foreground) !important;
|
||||
}
|
||||
.pf-c-table__sort-indicator i {
|
||||
color: var(--ak-dark-foreground) !important;
|
||||
}
|
||||
/* class for pagination text */
|
||||
.pf-c-options-menu__toggle {
|
||||
color: var(--ak-dark-foreground);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { gettext } from "django";
|
||||
import { customElement, html, property, TemplateResult } from "lit-element";
|
||||
import { PBResponse } from "../../api/Client";
|
||||
import { Table } from "../../elements/table/Table";
|
||||
import { Table, TableColumn } from "../../elements/table/Table";
|
||||
import { PolicyBinding } from "../../api/PolicyBindings";
|
||||
|
||||
import "../../elements/Tabs";
|
||||
|
@ -22,8 +22,14 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
|
|||
});
|
||||
}
|
||||
|
||||
columns(): string[] {
|
||||
return ["Policy", "Enabled", "Order", "Timeout", ""];
|
||||
columns(): TableColumn[] {
|
||||
return [
|
||||
new TableColumn("Policy"),
|
||||
new TableColumn("Enabled", "enabled"),
|
||||
new TableColumn("Order", "order"),
|
||||
new TableColumn("Timeout", "timeout"),
|
||||
new TableColumn(""),
|
||||
];
|
||||
}
|
||||
|
||||
row(item: PolicyBinding): TemplateResult[] {
|
||||
|
|
|
@ -6,10 +6,72 @@ import { COMMON_STYLES } from "../../common/styles";
|
|||
import "./TablePagination";
|
||||
import "../EmptyState";
|
||||
|
||||
|
||||
export class TableColumn {
|
||||
|
||||
title: string;
|
||||
orderBy?: string;
|
||||
|
||||
onClick?: () => void;
|
||||
|
||||
constructor(title: string, orderBy?: string) {
|
||||
this.title = title;
|
||||
this.orderBy = orderBy;
|
||||
}
|
||||
|
||||
headerClickHandler(table: Table<unknown>): void {
|
||||
if (!this.orderBy) {
|
||||
return;
|
||||
}
|
||||
if (table.order === this.orderBy) {
|
||||
table.order = `-${this.orderBy}`;
|
||||
} else {
|
||||
table.order = this.orderBy;
|
||||
}
|
||||
table.fetch();
|
||||
}
|
||||
|
||||
private getSortIndicator(table: Table<unknown>): string {
|
||||
switch (table.order) {
|
||||
case this.orderBy:
|
||||
return "fa-long-arrow-alt-down";
|
||||
case `-${this.orderBy}`:
|
||||
return "fa-long-arrow-alt-up";
|
||||
default:
|
||||
return "fa-arrows-alt-v";
|
||||
}
|
||||
}
|
||||
|
||||
renderSortable(table: Table<unknown>): TemplateResult {
|
||||
return html`
|
||||
<button class="pf-c-table__button" @click=${() => this.headerClickHandler(table)}>
|
||||
<div class="pf-c-table__button-content">
|
||||
<span class="pf-c-table__text">${gettext(this.title)}</span>
|
||||
<span class="pf-c-table__sort-indicator">
|
||||
<i class="fas ${this.getSortIndicator(table)}"></i>
|
||||
</span>
|
||||
</div>
|
||||
</button>`;
|
||||
}
|
||||
|
||||
render(table: Table<unknown>): TemplateResult {
|
||||
return html`<th
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
class="
|
||||
${this.orderBy ? "pf-c-table__sort " : " "}
|
||||
${(table.order === this.orderBy || table.order === `-${this.orderBy}`) ? "pf-m-selected " : ""}
|
||||
">
|
||||
${this.orderBy ? this.renderSortable(table) : html`${gettext(this.title)}`}
|
||||
</th>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export abstract class Table<T> extends LitElement {
|
||||
abstract apiEndpoint(page: number): Promise<PBResponse<T>>;
|
||||
abstract columns(): Array<string>;
|
||||
abstract row(item: T): Array<TemplateResult>;
|
||||
abstract columns(): TableColumn[];
|
||||
abstract row(item: T): TemplateResult[];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
renderExpanded(item: T): TemplateResult {
|
||||
|
@ -25,6 +87,12 @@ export abstract class Table<T> extends LitElement {
|
|||
@property({type: Number})
|
||||
page = 1;
|
||||
|
||||
@property({type: String})
|
||||
order?: string;
|
||||
|
||||
@property({type: String})
|
||||
search?: string;
|
||||
|
||||
@property({type: Boolean})
|
||||
expandable = false;
|
||||
|
||||
|
@ -43,6 +111,7 @@ export abstract class Table<T> extends LitElement {
|
|||
}
|
||||
|
||||
public fetch(): void {
|
||||
this.data = undefined;
|
||||
this.apiEndpoint(this.page).then((r) => {
|
||||
this.data = r;
|
||||
this.page = r.pagination.current;
|
||||
|
@ -123,12 +192,17 @@ export abstract class Table<T> extends LitElement {
|
|||
</button>`;
|
||||
}
|
||||
|
||||
renderSearch(): TemplateResult {
|
||||
return html``;
|
||||
}
|
||||
|
||||
renderTable(): TemplateResult {
|
||||
if (!this.data) {
|
||||
this.fetch();
|
||||
}
|
||||
return html`<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
${this.renderSearch()}
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
${this.renderToolbar()}
|
||||
</div>
|
||||
|
@ -143,7 +217,7 @@ export abstract class Table<T> extends LitElement {
|
|||
<thead>
|
||||
<tr role="row">
|
||||
${this.expandable ? html`<td role="cell">` : html``}
|
||||
${this.columns().map((col) => html`<th role="columnheader" scope="col">${gettext(col)}</th>`)}
|
||||
${this.columns().map((col) => col.render(this))}
|
||||
</tr>
|
||||
</thead>
|
||||
${this.data ? this.renderRows() : this.renderLoading()}
|
||||
|
|
|
@ -1,19 +1,34 @@
|
|||
import { html, TemplateResult } from "lit-html";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
import { Table } from "./Table";
|
||||
import "./TableSearch";
|
||||
|
||||
export abstract class TablePage<T> extends Table<T> {
|
||||
abstract pageTitle(): string;
|
||||
abstract pageDescription(): string;
|
||||
abstract pageDescription(): string | undefined;
|
||||
abstract pageIcon(): string;
|
||||
abstract searchEnabled(): boolean;
|
||||
|
||||
renderSearch(): TemplateResult {
|
||||
if (!this.searchEnabled()) {
|
||||
return super.renderSearch();
|
||||
}
|
||||
return html`<ak-table-search value=${ifDefined(this.search)} .onSearch=${(value: string) => {
|
||||
this.search = value;
|
||||
this.fetch();
|
||||
}}>
|
||||
</ak-table-search>`;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
const description = this.pageDescription();
|
||||
return html`<section class="pf-c-page__main-section pf-m-light">
|
||||
<div class="pf-c-content">
|
||||
<h1>
|
||||
<i class="${this.pageIcon()}"></i>
|
||||
${this.pageTitle()}
|
||||
</h1>
|
||||
<p>${this.pageDescription()}</p>
|
||||
${description ? html`<p>${description}</p>` : html``}
|
||||
</div>
|
||||
</section>
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
import { COMMON_STYLES } from "../../common/styles";
|
||||
|
||||
@customElement("ak-table-search")
|
||||
export class TableSearch extends LitElement {
|
||||
|
||||
@property()
|
||||
value?: string;
|
||||
|
||||
@property()
|
||||
onSearch?: (value: string) => void;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return COMMON_STYLES;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<div class="pf-c-toolbar__group pf-m-filter-group">
|
||||
<div class="pf-c-toolbar__item pf-m-search-filter">
|
||||
<form class="pf-c-input-group" method="GET" @submit=${(e: Event) => {
|
||||
e.preventDefault();
|
||||
if (!this.onSearch) return;
|
||||
const el = this.shadowRoot?.querySelector<HTMLInputElement>("input[type=search]");
|
||||
if (!el) return;
|
||||
if (el.value === "") return;
|
||||
this.onSearch(el?.value);
|
||||
}}>
|
||||
<input class="pf-c-form-control" name="search" type="search" placeholder="Search..." value="${ifDefined(this.value)}" @search=${() => {
|
||||
if (!this.onSearch) return;
|
||||
this.onSearch("");
|
||||
}}>
|
||||
<button class="pf-c-button pf-m-control" type="submit">
|
||||
<i class="fas fa-search" aria-hidden="true"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
}
|
|
@ -14,7 +14,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
|
|||
return User.me().then(u => u.is_superuser);
|
||||
}),
|
||||
new SidebarItem("Administration").children(
|
||||
new SidebarItem("Applications", "/administration/applications/").activeWhen(
|
||||
new SidebarItem("Applications", "/applications/").activeWhen(
|
||||
`^/applications/(?<slug>${SLUG_REGEX})/$`
|
||||
),
|
||||
new SidebarItem("Sources", "/administration/sources/").activeWhen(
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
import { gettext } from "django";
|
||||
import { customElement, html, TemplateResult } from "lit-element";
|
||||
import { customElement, html, property, TemplateResult } from "lit-element";
|
||||
import { Application } from "../../api/Applications";
|
||||
import { PBResponse } from "../../api/Client";
|
||||
import { TablePage } from "../../elements/table/TablePage";
|
||||
|
||||
import "../../elements/buttons/ModalButton";
|
||||
import "../../elements/buttons/SpinnerButton";
|
||||
import { TableColumn } from "../../elements/table/Table";
|
||||
|
||||
@customElement("ak-application-list")
|
||||
export class ApplicationList extends TablePage<Application> {
|
||||
searchEnabled(): boolean {
|
||||
return true;
|
||||
}
|
||||
pageTitle(): string {
|
||||
return gettext("Applications");
|
||||
}
|
||||
|
@ -19,31 +23,51 @@ export class ApplicationList extends TablePage<Application> {
|
|||
return gettext("pf-icon pf-icon-applications");
|
||||
}
|
||||
|
||||
@property()
|
||||
order = "name";
|
||||
|
||||
apiEndpoint(page: number): Promise<PBResponse<Application>> {
|
||||
return Application.list({
|
||||
ordering: "order",
|
||||
ordering: this.order,
|
||||
page: page,
|
||||
search: this.search || "",
|
||||
});
|
||||
}
|
||||
|
||||
columns(): string[] {
|
||||
return ["Name", "Slug", "Provider", "Provider Type", ""];
|
||||
columns(): TableColumn[] {
|
||||
return [
|
||||
new TableColumn(""),
|
||||
new TableColumn("Name", "name"),
|
||||
new TableColumn("Slug", "slug"),
|
||||
new TableColumn("Provider"),
|
||||
new TableColumn("Provider Type"),
|
||||
new TableColumn(""),
|
||||
];
|
||||
}
|
||||
|
||||
row(item: Application): TemplateResult[] {
|
||||
return [
|
||||
html`${item.name}`,
|
||||
html`${item.slug}`,
|
||||
html`${item.provider}`,
|
||||
html`${item.provider}`,
|
||||
html`
|
||||
<ak-modal-button href="administration/policies/bindings/${item.pk}/update/">
|
||||
${item.meta_icon ?
|
||||
html`<img class="app-icon pf-c-avatar" src="${item.meta_icon}" alt="${gettext("Application Icon")}">` :
|
||||
html`<i class="pf-icon pf-icon-arrow"></i>`}`,
|
||||
html`<a href="#/applications/${item.slug}/">
|
||||
<div>
|
||||
${item.name}
|
||||
</div>
|
||||
${item.meta_publisher ? html`<small>${item.meta_publisher}</small>` : html``}
|
||||
</a>`,
|
||||
html`<code>${item.slug}</code>`,
|
||||
html`${item.provider.name}`,
|
||||
html`${item.provider.verbose_name}`,
|
||||
html`
|
||||
<ak-modal-button href="${Application.adminUrl(`${item.pk}/update/`)}">
|
||||
<ak-spinner-button slot="trigger" class="pf-m-secondary">
|
||||
Edit
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
<ak-modal-button href="administration/policies/bindings/${item.pk}/delete/">
|
||||
<ak-modal-button href="${Application.adminUrl(`${item.pk}/delete/`)}">
|
||||
<ak-spinner-button slot="trigger" class="pf-m-danger">
|
||||
Delete
|
||||
</ak-spinner-button>
|
||||
|
@ -52,4 +76,16 @@ export class ApplicationList extends TablePage<Application> {
|
|||
`,
|
||||
];
|
||||
}
|
||||
|
||||
renderToolbar(): TemplateResult {
|
||||
return html`
|
||||
<ak-modal-button href=${Application.adminUrl("create/")}>
|
||||
<ak-spinner-button slot="trigger" class="pf-m-primary">
|
||||
${gettext("Create")}
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
${super.renderToolbar()}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { gettext } from "django";
|
||||
import { customElement, html, property, TemplateResult } from "lit-element";
|
||||
import { PBResponse } from "../../api/Client";
|
||||
import { Table } from "../../elements/table/Table";
|
||||
import { Table, TableColumn } from "../../elements/table/Table";
|
||||
|
||||
import "../../elements/Tabs";
|
||||
import "../../elements/AdminLoginsChart";
|
||||
|
@ -25,8 +25,13 @@ export class BoundStagesList extends Table<FlowStageBinding> {
|
|||
});
|
||||
}
|
||||
|
||||
columns(): string[] {
|
||||
return ["Order", "Name", "Type", ""];
|
||||
columns(): TableColumn[] {
|
||||
return [
|
||||
new TableColumn("Order"),
|
||||
new TableColumn("Name"),
|
||||
new TableColumn("Type"),
|
||||
new TableColumn(""),
|
||||
];
|
||||
}
|
||||
|
||||
row(item: FlowStageBinding): TemplateResult[] {
|
||||
|
|
Reference in New Issue