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 datetime import timedelta
|
||||||
from typing import Dict, List
|
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.fields import DurationField
|
||||||
from django.db.models.functions import ExtractHour
|
from django.db.models.functions import ExtractHour
|
||||||
from django.http import response
|
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from drf_yasg2.utils import swagger_auto_schema
|
from drf_yasg2.utils import swagger_auto_schema
|
||||||
from rest_framework.fields import SerializerMethodField
|
from rest_framework.fields import SerializerMethodField
|
||||||
|
@ -60,10 +59,10 @@ class AdministrationMetricsSerializer(Serializer):
|
||||||
"""Get failed logins per hour for the last 24 hours"""
|
"""Get failed logins per hour for the last 24 hours"""
|
||||||
return get_events_per_1h(action=EventAction.LOGIN_FAILED)
|
return get_events_per_1h(action=EventAction.LOGIN_FAILED)
|
||||||
|
|
||||||
def create(self, request: Request) -> response:
|
def create(self, validated_data: dict) -> Model:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def update(self, request: Request) -> Response:
|
def update(self, instance: Model, validated_data: dict) -> Model:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
|
from django.db.models import Model
|
||||||
from django.http.response import Http404
|
from django.http.response import Http404
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from drf_yasg2.utils import swagger_auto_schema
|
from drf_yasg2.utils import swagger_auto_schema
|
||||||
|
@ -26,10 +27,10 @@ class TaskSerializer(Serializer):
|
||||||
status = IntegerField(source="result.status.value")
|
status = IntegerField(source="result.status.value")
|
||||||
messages = ListField(source="result.messages")
|
messages = ListField(source="result.messages")
|
||||||
|
|
||||||
def create(self, request: Request) -> Response:
|
def create(self, validated_data: dict) -> Model:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def update(self, request: Request) -> Response:
|
def update(self, instance: Model, validated_data: dict) -> Model:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
"""authentik administration overview"""
|
"""authentik administration overview"""
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
from django.db.models import Model
|
||||||
from drf_yasg2.utils import swagger_auto_schema
|
from drf_yasg2.utils import swagger_auto_schema
|
||||||
from packaging.version import parse
|
from packaging.version import parse
|
||||||
from rest_framework.fields import SerializerMethodField
|
from rest_framework.fields import SerializerMethodField
|
||||||
|
@ -39,10 +40,10 @@ class VersionSerializer(Serializer):
|
||||||
self.get_version_latest(instance)
|
self.get_version_latest(instance)
|
||||||
)
|
)
|
||||||
|
|
||||||
def create(self, request: Request) -> Response:
|
def create(self, validated_data: dict) -> Model:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def update(self, request: Request) -> Response:
|
def update(self, instance: Model, validated_data: dict) -> Model:
|
||||||
raise NotImplementedError
|
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",
|
name="overview-clear-policy-cache",
|
||||||
),
|
),
|
||||||
# Applications
|
# Applications
|
||||||
path(
|
|
||||||
"applications/", applications.ApplicationListView.as_view(), name="applications"
|
|
||||||
),
|
|
||||||
path(
|
path(
|
||||||
"applications/create/",
|
"applications/create/",
|
||||||
applications.ApplicationCreateView.as_view(),
|
applications.ApplicationCreateView.as_view(),
|
||||||
|
|
|
@ -6,44 +6,18 @@ from django.contrib.auth.mixins import (
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic import ListView, UpdateView
|
from django.views.generic import UpdateView
|
||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
from guardian.mixins import PermissionRequiredMixin
|
||||||
|
|
||||||
from authentik.admin.views.utils import (
|
from authentik.admin.views.utils import (
|
||||||
BackSuccessUrlMixin,
|
BackSuccessUrlMixin,
|
||||||
DeleteMessageView,
|
DeleteMessageView,
|
||||||
SearchListMixin,
|
|
||||||
UserPaginateListMixin,
|
|
||||||
)
|
)
|
||||||
from authentik.core.forms.applications import ApplicationForm
|
from authentik.core.forms.applications import ApplicationForm
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.lib.views import CreateAssignPermView
|
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(
|
class ApplicationCreateView(
|
||||||
SuccessMessageMixin,
|
SuccessMessageMixin,
|
||||||
BackSuccessUrlMixin,
|
BackSuccessUrlMixin,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
"""core Configs API"""
|
"""core Configs API"""
|
||||||
|
from django.db.models import Model
|
||||||
from drf_yasg2.utils import swagger_auto_schema
|
from drf_yasg2.utils import swagger_auto_schema
|
||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import AllowAny
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
|
@ -19,10 +20,10 @@ class ConfigSerializer(Serializer):
|
||||||
error_reporting_environment = ReadOnlyField()
|
error_reporting_environment = ReadOnlyField()
|
||||||
error_reporting_send_pii = ReadOnlyField()
|
error_reporting_send_pii = ReadOnlyField()
|
||||||
|
|
||||||
def create(self, request: Request) -> Response:
|
def create(self, validated_data: dict) -> Model:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def update(self, request: Request) -> Response:
|
def update(self, instance: Model, validated_data: dict) -> Model:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
"""core messages API"""
|
"""core messages API"""
|
||||||
from django.contrib.messages import get_messages
|
from django.contrib.messages import get_messages
|
||||||
|
from django.db.models import Model
|
||||||
from drf_yasg2.utils import swagger_auto_schema
|
from drf_yasg2.utils import swagger_auto_schema
|
||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import AllowAny
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
|
@ -17,10 +18,10 @@ class MessageSerializer(Serializer):
|
||||||
extra_tags = ReadOnlyField()
|
extra_tags = ReadOnlyField()
|
||||||
level_tag = ReadOnlyField()
|
level_tag = ReadOnlyField()
|
||||||
|
|
||||||
def create(self, request: Request) -> Response:
|
def create(self, validated_data: dict) -> Model:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def update(self, request: Request) -> Response:
|
def update(self, instance: Model, validated_data: dict) -> Model:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ from rest_framework.viewsets import ModelViewSet
|
||||||
from rest_framework_guardian.filters import ObjectPermissionsFilter
|
from rest_framework_guardian.filters import ObjectPermissionsFilter
|
||||||
|
|
||||||
from authentik.admin.api.metrics import get_events_per_1h
|
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.core.models import Application
|
||||||
from authentik.events.models import EventAction
|
from authentik.events.models import EventAction
|
||||||
from authentik.policies.engine import PolicyEngine
|
from authentik.policies.engine import PolicyEngine
|
||||||
|
@ -21,6 +22,7 @@ class ApplicationSerializer(ModelSerializer):
|
||||||
"""Application Serializer"""
|
"""Application Serializer"""
|
||||||
|
|
||||||
launch_url = SerializerMethodField()
|
launch_url = SerializerMethodField()
|
||||||
|
provider = ProviderSerializer(source="get_provider")
|
||||||
|
|
||||||
def get_launch_url(self, instance: Application) -> str:
|
def get_launch_url(self, instance: Application) -> str:
|
||||||
"""Get generated launch URL"""
|
"""Get generated launch URL"""
|
||||||
|
@ -48,7 +50,15 @@ class ApplicationViewSet(ModelViewSet):
|
||||||
|
|
||||||
queryset = Application.objects.all()
|
queryset = Application.objects.all()
|
||||||
serializer_class = ApplicationSerializer
|
serializer_class = ApplicationSerializer
|
||||||
|
search_fields = [
|
||||||
|
"name",
|
||||||
|
"slug",
|
||||||
|
"meta_launch_url",
|
||||||
|
"meta_description",
|
||||||
|
"meta_publisher",
|
||||||
|
]
|
||||||
lookup_field = "slug"
|
lookup_field = "slug"
|
||||||
|
ordering = ["name"]
|
||||||
|
|
||||||
def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet:
|
def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet:
|
||||||
"""Custom filter_queryset method which ignores guardian, but still supports sorting"""
|
"""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())
|
queryset = self._filter_queryset_for_list(self.get_queryset())
|
||||||
self.paginate_queryset(queryset)
|
self.paginate_queryset(queryset)
|
||||||
allowed_applications = []
|
allowed_applications = []
|
||||||
for application in queryset.order_by("name"):
|
for application in queryset:
|
||||||
engine = PolicyEngine(application, self.request.user, self.request)
|
engine = PolicyEngine(application, self.request.user, self.request)
|
||||||
engine.build()
|
engine.build()
|
||||||
if engine.passing:
|
if engine.passing:
|
||||||
|
|
|
@ -2,15 +2,16 @@
|
||||||
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.core.api.utils import MetaNameSerializer
|
||||||
from authentik.core.models import Provider
|
from authentik.core.models import Provider
|
||||||
|
|
||||||
|
|
||||||
class ProviderSerializer(ModelSerializer):
|
class ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
||||||
"""Provider Serializer"""
|
"""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"""
|
"""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", "")
|
return obj._meta.object_name.lower().replace("provider", "")
|
||||||
|
|
||||||
|
@ -29,7 +30,9 @@ class ProviderSerializer(ModelSerializer):
|
||||||
"application",
|
"application",
|
||||||
"authorization_flow",
|
"authorization_flow",
|
||||||
"property_mappings",
|
"property_mappings",
|
||||||
"__type__",
|
"object_type",
|
||||||
|
"verbose_name",
|
||||||
|
"verbose_name_plural",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
24
authentik/core/api/utils.py
Normal file
24
authentik/core/api/utils.py
Normal file
|
@ -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.serializers import ModelSerializer
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.core.api.utils import MetaNameSerializer
|
||||||
from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping
|
from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping
|
||||||
|
|
||||||
|
|
||||||
class OAuth2ProviderSerializer(ModelSerializer):
|
class OAuth2ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
||||||
"""OAuth2Provider Serializer"""
|
"""OAuth2Provider Serializer"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -25,6 +26,8 @@ class OAuth2ProviderSerializer(ModelSerializer):
|
||||||
"redirect_uris",
|
"redirect_uris",
|
||||||
"sub_mode",
|
"sub_mode",
|
||||||
"property_mappings",
|
"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.serializers import ModelSerializer, Serializer
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.core.api.utils import MetaNameSerializer
|
||||||
from authentik.providers.oauth2.views.provider import ProviderInfoView
|
from authentik.providers.oauth2.views.provider import ProviderInfoView
|
||||||
from authentik.providers.proxy.models import ProxyProvider
|
from authentik.providers.proxy.models import ProxyProvider
|
||||||
|
|
||||||
|
@ -33,7 +34,7 @@ class OpenIDConnectConfigurationSerializer(Serializer):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class ProxyProviderSerializer(ModelSerializer):
|
class ProxyProviderSerializer(MetaNameSerializer, ModelSerializer):
|
||||||
"""ProxyProvider Serializer"""
|
"""ProxyProvider Serializer"""
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
|
@ -60,6 +61,8 @@ class ProxyProviderSerializer(ModelSerializer):
|
||||||
"basic_auth_enabled",
|
"basic_auth_enabled",
|
||||||
"basic_auth_password_attribute",
|
"basic_auth_password_attribute",
|
||||||
"basic_auth_user_attribute",
|
"basic_auth_user_attribute",
|
||||||
|
"verbose_name",
|
||||||
|
"verbose_name_plural",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,11 @@
|
||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.core.api.utils import MetaNameSerializer
|
||||||
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
|
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
|
||||||
|
|
||||||
|
|
||||||
class SAMLProviderSerializer(ModelSerializer):
|
class SAMLProviderSerializer(ModelSerializer, MetaNameSerializer):
|
||||||
"""SAMLProvider Serializer"""
|
"""SAMLProvider Serializer"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -25,6 +26,8 @@ class SAMLProviderSerializer(ModelSerializer):
|
||||||
"signature_algorithm",
|
"signature_algorithm",
|
||||||
"signing_kp",
|
"signing_kp",
|
||||||
"verification_kp",
|
"verification_kp",
|
||||||
|
"verbose_name",
|
||||||
|
"verbose_name_plural",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -148,9 +148,9 @@ class SAMLProvider(Provider):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def serializer(self) -> Type[Serializer]:
|
def serializer(self) -> Type[Serializer]:
|
||||||
from authentik.providers.saml.api import SAMLPropertyMappingSerializer
|
from authentik.providers.saml.api import SAMLProviderSerializer
|
||||||
|
|
||||||
return SAMLPropertyMappingSerializer
|
return SAMLProviderSerializer
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def form(self) -> Type[ModelForm]:
|
def form(self) -> Type[ModelForm]:
|
||||||
|
|
106
swagger.yaml
106
swagger.yaml
|
@ -6729,11 +6729,55 @@ definitions:
|
||||||
title: Outdated
|
title: Outdated
|
||||||
type: boolean
|
type: boolean
|
||||||
readOnly: true
|
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:
|
Application:
|
||||||
description: Application Serializer
|
description: Application Serializer
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
- slug
|
- slug
|
||||||
|
- provider
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
pk:
|
pk:
|
||||||
|
@ -6755,9 +6799,7 @@ definitions:
|
||||||
maxLength: 50
|
maxLength: 50
|
||||||
minLength: 1
|
minLength: 1
|
||||||
provider:
|
provider:
|
||||||
title: Provider
|
$ref: '#/definitions/Provider'
|
||||||
type: integer
|
|
||||||
x-nullable: true
|
|
||||||
launch_url:
|
launch_url:
|
||||||
title: Launch url
|
title: Launch url
|
||||||
type: string
|
type: string
|
||||||
|
@ -7720,40 +7762,6 @@ definitions:
|
||||||
title: Expression
|
title: Expression
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
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:
|
OAuth2Provider:
|
||||||
description: OAuth2Provider Serializer
|
description: OAuth2Provider Serializer
|
||||||
required:
|
required:
|
||||||
|
@ -7845,6 +7853,14 @@ definitions:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
uniqueItems: true
|
uniqueItems: true
|
||||||
|
verbose_name:
|
||||||
|
title: Verbose name
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
verbose_name_plural:
|
||||||
|
title: Verbose name plural
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
ProxyProvider:
|
ProxyProvider:
|
||||||
description: ProxyProvider Serializer
|
description: ProxyProvider Serializer
|
||||||
required:
|
required:
|
||||||
|
@ -7898,6 +7914,14 @@ definitions:
|
||||||
description: User/Group Attribute used for the user part of the HTTP-Basic
|
description: User/Group Attribute used for the user part of the HTTP-Basic
|
||||||
Header. If not set, the user's Email address is used.
|
Header. If not set, the user's Email address is used.
|
||||||
type: string
|
type: string
|
||||||
|
verbose_name:
|
||||||
|
title: Verbose name
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
verbose_name_plural:
|
||||||
|
title: Verbose name plural
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
SAMLProvider:
|
SAMLProvider:
|
||||||
description: SAMLProvider Serializer
|
description: SAMLProvider Serializer
|
||||||
required:
|
required:
|
||||||
|
@ -7984,6 +8008,14 @@ definitions:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
x-nullable: true
|
x-nullable: true
|
||||||
|
verbose_name:
|
||||||
|
title: Verbose name
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
verbose_name_plural:
|
||||||
|
title: Verbose name plural
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
Config:
|
Config:
|
||||||
description: Serialize authentik Config into DRF Object
|
description: Serialize authentik Config into DRF Object
|
||||||
type: object
|
type: object
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { DefaultClient, PBResponse, QueryArguments } from "./Client";
|
import { DefaultClient, PBResponse, QueryArguments } from "./Client";
|
||||||
|
import { Provider } from "./Providers";
|
||||||
|
|
||||||
export class Application {
|
export class Application {
|
||||||
pk: string;
|
pk: string;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
provider: number;
|
provider: Provider;
|
||||||
|
|
||||||
launch_url: string;
|
launch_url: string;
|
||||||
meta_launch_url: string;
|
meta_launch_url: string;
|
||||||
|
@ -24,4 +25,8 @@ export class Application {
|
||||||
static list(filter?: QueryArguments): Promise<PBResponse<Application>> {
|
static list(filter?: QueryArguments): Promise<PBResponse<Application>> {
|
||||||
return DefaultClient.fetch<PBResponse<Application>>(["core", "applications"], filter);
|
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;
|
pk: number;
|
||||||
name: string;
|
name: string;
|
||||||
authorization_flow: string;
|
authorization_flow: string;
|
||||||
|
verbose_name: string;
|
||||||
|
verbose_name_plural: string;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
throw Error();
|
throw Error();
|
||||||
|
|
|
@ -136,6 +136,12 @@ select[multiple] {
|
||||||
--pf-c-table--BorderColor: var(--ak-dark-background-lighter);
|
--pf-c-table--BorderColor: var(--ak-dark-background-lighter);
|
||||||
--pf-c-table--cell--Color: var(--ak-dark-foreground);
|
--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 */
|
/* class for pagination text */
|
||||||
.pf-c-options-menu__toggle {
|
.pf-c-options-menu__toggle {
|
||||||
color: var(--ak-dark-foreground);
|
color: var(--ak-dark-foreground);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { gettext } from "django";
|
import { gettext } from "django";
|
||||||
import { customElement, html, property, TemplateResult } from "lit-element";
|
import { customElement, html, property, TemplateResult } from "lit-element";
|
||||||
import { PBResponse } from "../../api/Client";
|
import { PBResponse } from "../../api/Client";
|
||||||
import { Table } from "../../elements/table/Table";
|
import { Table, TableColumn } from "../../elements/table/Table";
|
||||||
import { PolicyBinding } from "../../api/PolicyBindings";
|
import { PolicyBinding } from "../../api/PolicyBindings";
|
||||||
|
|
||||||
import "../../elements/Tabs";
|
import "../../elements/Tabs";
|
||||||
|
@ -22,8 +22,14 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
columns(): string[] {
|
columns(): TableColumn[] {
|
||||||
return ["Policy", "Enabled", "Order", "Timeout", ""];
|
return [
|
||||||
|
new TableColumn("Policy"),
|
||||||
|
new TableColumn("Enabled", "enabled"),
|
||||||
|
new TableColumn("Order", "order"),
|
||||||
|
new TableColumn("Timeout", "timeout"),
|
||||||
|
new TableColumn(""),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
row(item: PolicyBinding): TemplateResult[] {
|
row(item: PolicyBinding): TemplateResult[] {
|
||||||
|
|
|
@ -6,10 +6,72 @@ import { COMMON_STYLES } from "../../common/styles";
|
||||||
import "./TablePagination";
|
import "./TablePagination";
|
||||||
import "../EmptyState";
|
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 {
|
export abstract class Table<T> extends LitElement {
|
||||||
abstract apiEndpoint(page: number): Promise<PBResponse<T>>;
|
abstract apiEndpoint(page: number): Promise<PBResponse<T>>;
|
||||||
abstract columns(): Array<string>;
|
abstract columns(): TableColumn[];
|
||||||
abstract row(item: T): Array<TemplateResult>;
|
abstract row(item: T): TemplateResult[];
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
renderExpanded(item: T): TemplateResult {
|
renderExpanded(item: T): TemplateResult {
|
||||||
|
@ -25,6 +87,12 @@ export abstract class Table<T> extends LitElement {
|
||||||
@property({type: Number})
|
@property({type: Number})
|
||||||
page = 1;
|
page = 1;
|
||||||
|
|
||||||
|
@property({type: String})
|
||||||
|
order?: string;
|
||||||
|
|
||||||
|
@property({type: String})
|
||||||
|
search?: string;
|
||||||
|
|
||||||
@property({type: Boolean})
|
@property({type: Boolean})
|
||||||
expandable = false;
|
expandable = false;
|
||||||
|
|
||||||
|
@ -43,6 +111,7 @@ export abstract class Table<T> extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetch(): void {
|
public fetch(): void {
|
||||||
|
this.data = undefined;
|
||||||
this.apiEndpoint(this.page).then((r) => {
|
this.apiEndpoint(this.page).then((r) => {
|
||||||
this.data = r;
|
this.data = r;
|
||||||
this.page = r.pagination.current;
|
this.page = r.pagination.current;
|
||||||
|
@ -123,12 +192,17 @@ export abstract class Table<T> extends LitElement {
|
||||||
</button>`;
|
</button>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderSearch(): TemplateResult {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
renderTable(): TemplateResult {
|
renderTable(): TemplateResult {
|
||||||
if (!this.data) {
|
if (!this.data) {
|
||||||
this.fetch();
|
this.fetch();
|
||||||
}
|
}
|
||||||
return html`<div class="pf-c-toolbar">
|
return html`<div class="pf-c-toolbar">
|
||||||
<div class="pf-c-toolbar__content">
|
<div class="pf-c-toolbar__content">
|
||||||
|
${this.renderSearch()}
|
||||||
<div class="pf-c-toolbar__bulk-select">
|
<div class="pf-c-toolbar__bulk-select">
|
||||||
${this.renderToolbar()}
|
${this.renderToolbar()}
|
||||||
</div>
|
</div>
|
||||||
|
@ -143,7 +217,7 @@ export abstract class Table<T> extends LitElement {
|
||||||
<thead>
|
<thead>
|
||||||
<tr role="row">
|
<tr role="row">
|
||||||
${this.expandable ? html`<td role="cell">` : html``}
|
${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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
${this.data ? this.renderRows() : this.renderLoading()}
|
${this.data ? this.renderRows() : this.renderLoading()}
|
||||||
|
|
|
@ -1,19 +1,34 @@
|
||||||
import { html, TemplateResult } from "lit-html";
|
import { html, TemplateResult } from "lit-html";
|
||||||
|
import { ifDefined } from "lit-html/directives/if-defined";
|
||||||
import { Table } from "./Table";
|
import { Table } from "./Table";
|
||||||
|
import "./TableSearch";
|
||||||
|
|
||||||
export abstract class TablePage<T> extends Table<T> {
|
export abstract class TablePage<T> extends Table<T> {
|
||||||
abstract pageTitle(): string;
|
abstract pageTitle(): string;
|
||||||
abstract pageDescription(): string;
|
abstract pageDescription(): string | undefined;
|
||||||
abstract pageIcon(): string;
|
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 {
|
render(): TemplateResult {
|
||||||
|
const description = this.pageDescription();
|
||||||
return html`<section class="pf-c-page__main-section pf-m-light">
|
return html`<section class="pf-c-page__main-section pf-m-light">
|
||||||
<div class="pf-c-content">
|
<div class="pf-c-content">
|
||||||
<h1>
|
<h1>
|
||||||
<i class="${this.pageIcon()}"></i>
|
<i class="${this.pageIcon()}"></i>
|
||||||
${this.pageTitle()}
|
${this.pageTitle()}
|
||||||
</h1>
|
</h1>
|
||||||
<p>${this.pageDescription()}</p>
|
${description ? html`<p>${description}</p>` : html``}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||||
|
|
41
web/src/elements/table/TableSearch.ts
Normal file
41
web/src/elements/table/TableSearch.ts
Normal file
|
@ -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);
|
return User.me().then(u => u.is_superuser);
|
||||||
}),
|
}),
|
||||||
new SidebarItem("Administration").children(
|
new SidebarItem("Administration").children(
|
||||||
new SidebarItem("Applications", "/administration/applications/").activeWhen(
|
new SidebarItem("Applications", "/applications/").activeWhen(
|
||||||
`^/applications/(?<slug>${SLUG_REGEX})/$`
|
`^/applications/(?<slug>${SLUG_REGEX})/$`
|
||||||
),
|
),
|
||||||
new SidebarItem("Sources", "/administration/sources/").activeWhen(
|
new SidebarItem("Sources", "/administration/sources/").activeWhen(
|
||||||
|
|
|
@ -1,14 +1,18 @@
|
||||||
import { gettext } from "django";
|
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 { Application } from "../../api/Applications";
|
||||||
import { PBResponse } from "../../api/Client";
|
import { PBResponse } from "../../api/Client";
|
||||||
import { TablePage } from "../../elements/table/TablePage";
|
import { TablePage } from "../../elements/table/TablePage";
|
||||||
|
|
||||||
import "../../elements/buttons/ModalButton";
|
import "../../elements/buttons/ModalButton";
|
||||||
import "../../elements/buttons/SpinnerButton";
|
import "../../elements/buttons/SpinnerButton";
|
||||||
|
import { TableColumn } from "../../elements/table/Table";
|
||||||
|
|
||||||
@customElement("ak-application-list")
|
@customElement("ak-application-list")
|
||||||
export class ApplicationList extends TablePage<Application> {
|
export class ApplicationList extends TablePage<Application> {
|
||||||
|
searchEnabled(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
pageTitle(): string {
|
pageTitle(): string {
|
||||||
return gettext("Applications");
|
return gettext("Applications");
|
||||||
}
|
}
|
||||||
|
@ -19,31 +23,51 @@ export class ApplicationList extends TablePage<Application> {
|
||||||
return gettext("pf-icon pf-icon-applications");
|
return gettext("pf-icon pf-icon-applications");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@property()
|
||||||
|
order = "name";
|
||||||
|
|
||||||
apiEndpoint(page: number): Promise<PBResponse<Application>> {
|
apiEndpoint(page: number): Promise<PBResponse<Application>> {
|
||||||
return Application.list({
|
return Application.list({
|
||||||
ordering: "order",
|
ordering: this.order,
|
||||||
page: page,
|
page: page,
|
||||||
|
search: this.search || "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
columns(): string[] {
|
columns(): TableColumn[] {
|
||||||
return ["Name", "Slug", "Provider", "Provider Type", ""];
|
return [
|
||||||
|
new TableColumn(""),
|
||||||
|
new TableColumn("Name", "name"),
|
||||||
|
new TableColumn("Slug", "slug"),
|
||||||
|
new TableColumn("Provider"),
|
||||||
|
new TableColumn("Provider Type"),
|
||||||
|
new TableColumn(""),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
row(item: Application): TemplateResult[] {
|
row(item: Application): TemplateResult[] {
|
||||||
return [
|
return [
|
||||||
html`${item.name}`,
|
|
||||||
html`${item.slug}`,
|
|
||||||
html`${item.provider}`,
|
|
||||||
html`${item.provider}`,
|
|
||||||
html`
|
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">
|
<ak-spinner-button slot="trigger" class="pf-m-secondary">
|
||||||
Edit
|
Edit
|
||||||
</ak-spinner-button>
|
</ak-spinner-button>
|
||||||
<div slot="modal"></div>
|
<div slot="modal"></div>
|
||||||
</ak-modal-button>
|
</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">
|
<ak-spinner-button slot="trigger" class="pf-m-danger">
|
||||||
Delete
|
Delete
|
||||||
</ak-spinner-button>
|
</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 { gettext } from "django";
|
||||||
import { customElement, html, property, TemplateResult } from "lit-element";
|
import { customElement, html, property, TemplateResult } from "lit-element";
|
||||||
import { PBResponse } from "../../api/Client";
|
import { PBResponse } from "../../api/Client";
|
||||||
import { Table } from "../../elements/table/Table";
|
import { Table, TableColumn } from "../../elements/table/Table";
|
||||||
|
|
||||||
import "../../elements/Tabs";
|
import "../../elements/Tabs";
|
||||||
import "../../elements/AdminLoginsChart";
|
import "../../elements/AdminLoginsChart";
|
||||||
|
@ -25,8 +25,13 @@ export class BoundStagesList extends Table<FlowStageBinding> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
columns(): string[] {
|
columns(): TableColumn[] {
|
||||||
return ["Order", "Name", "Type", ""];
|
return [
|
||||||
|
new TableColumn("Order"),
|
||||||
|
new TableColumn("Name"),
|
||||||
|
new TableColumn("Type"),
|
||||||
|
new TableColumn(""),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
row(item: FlowStageBinding): TemplateResult[] {
|
row(item: FlowStageBinding): TemplateResult[] {
|
||||||
|
|
Reference in a new issue