diff --git a/authentik/admin/api/metrics.py b/authentik/admin/api/metrics.py index 0344c21f5..3f2284980 100644 --- a/authentik/admin/api/metrics.py +++ b/authentik/admin/api/metrics.py @@ -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 diff --git a/authentik/admin/api/tasks.py b/authentik/admin/api/tasks.py index 314d7cae7..3c4075084 100644 --- a/authentik/admin/api/tasks.py +++ b/authentik/admin/api/tasks.py @@ -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 diff --git a/authentik/admin/api/version.py b/authentik/admin/api/version.py index 607de64b2..9201b2369 100644 --- a/authentik/admin/api/version.py +++ b/authentik/admin/api/version.py @@ -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 diff --git a/authentik/admin/templates/administration/application/list.html b/authentik/admin/templates/administration/application/list.html deleted file mode 100644 index a31f92f6b..000000000 --- a/authentik/admin/templates/administration/application/list.html +++ /dev/null @@ -1,131 +0,0 @@ -{% extends "administration/base.html" %} - -{% load i18n %} -{% load authentik_utils %} - -{% block content %} -
-
-

- - {% trans 'Applications' %} -

-

{% trans "External Applications which use authentik as Identity-Provider, utilizing protocols like OAuth2 and SAML." %}

-
-
-
-
- {% if object_list %} -
-
- {% include 'partials/toolbar_search.html' %} -
- - - {% trans 'Create' %} - -
-
- -
- {% include 'partials/pagination.html' %} -
-
- - - - - - - - - - - - - {% for application in object_list %} - - - - - - - - - {% endfor %} - -
{% trans 'Name' %}{% trans 'Slug' %}{% trans 'Provider' %}{% trans 'Provider Type' %}
- {% if application.meta_icon %} - {% trans 'Application Icon' %} - {% else %} - - {% endif %} - - -
- {{ application.name }} -
- {% if application.meta_publisher %} - {{ application.meta_publisher }} - {% endif %} -
-
- {{ application.slug }} - - - {{ application.get_provider }} - - - - {{ application.get_provider|verbose_name }} - - - - - {% trans 'Edit' %} - -
-
- - - {% trans 'Delete' %} - -
-
-
-
- {% include 'partials/pagination.html' %} -
- {% else %} -
-
- {% include 'partials/toolbar_search.html' %} -
-
-
-
- -

- {% trans 'No Applications.' %} -

-
- {% 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 %} -
- - - {% trans 'Create' %} - -
-
-
-
- {% endif %} -
-
-{% endblock %} diff --git a/authentik/admin/urls.py b/authentik/admin/urls.py index afcce4cc9..118457046 100644 --- a/authentik/admin/urls.py +++ b/authentik/admin/urls.py @@ -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(), diff --git a/authentik/admin/views/applications.py b/authentik/admin/views/applications.py index 4d440227b..c300c1604 100644 --- a/authentik/admin/views/applications.py +++ b/authentik/admin/views/applications.py @@ -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, diff --git a/authentik/api/v2/config.py b/authentik/api/v2/config.py index 89ec46b1f..012f20574 100644 --- a/authentik/api/v2/config.py +++ b/authentik/api/v2/config.py @@ -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 diff --git a/authentik/api/v2/messages.py b/authentik/api/v2/messages.py index 6fbc2702c..b2509559d 100644 --- a/authentik/api/v2/messages.py +++ b/authentik/api/v2/messages.py @@ -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 diff --git a/authentik/core/api/applications.py b/authentik/core/api/applications.py index 5136e09b8..689d045f9 100644 --- a/authentik/core/api/applications.py +++ b/authentik/core/api/applications.py @@ -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: diff --git a/authentik/core/api/providers.py b/authentik/core/api/providers.py index 29272c224..5892b5fa2 100644 --- a/authentik/core/api/providers.py +++ b/authentik/core/api/providers.py @@ -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", ] diff --git a/authentik/core/api/utils.py b/authentik/core/api/utils.py new file mode 100644 index 000000000..06c825856 --- /dev/null +++ b/authentik/core/api/utils.py @@ -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 diff --git a/authentik/providers/oauth2/api.py b/authentik/providers/oauth2/api.py index 91ae6711b..5f79b081c 100644 --- a/authentik/providers/oauth2/api.py +++ b/authentik/providers/oauth2/api.py @@ -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", ] diff --git a/authentik/providers/proxy/api.py b/authentik/providers/proxy/api.py index 51377c0a4..b1765bb28 100644 --- a/authentik/providers/proxy/api.py +++ b/authentik/providers/proxy/api.py @@ -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", ] diff --git a/authentik/providers/saml/api.py b/authentik/providers/saml/api.py index bf0f5501d..8443a6bf7 100644 --- a/authentik/providers/saml/api.py +++ b/authentik/providers/saml/api.py @@ -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", ] diff --git a/authentik/providers/saml/models.py b/authentik/providers/saml/models.py index eea7e9ee2..5fe37d055 100644 --- a/authentik/providers/saml/models.py +++ b/authentik/providers/saml/models.py @@ -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]: diff --git a/swagger.yaml b/swagger.yaml index 0d05fa15a..a60567fc3 100755 --- a/swagger.yaml +++ b/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 diff --git a/web/src/api/Applications.ts b/web/src/api/Applications.ts index 38379c605..2e0c8337c 100644 --- a/web/src/api/Applications.ts +++ b/web/src/api/Applications.ts @@ -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> { return DefaultClient.fetch>(["core", "applications"], filter); } + + static adminUrl(rest: string): string { + return `/administration/applications/${rest}`; + } } diff --git a/web/src/api/Providers.ts b/web/src/api/Providers.ts index e16b3b0b8..b2bee21e1 100644 --- a/web/src/api/Providers.ts +++ b/web/src/api/Providers.ts @@ -4,6 +4,8 @@ export class Provider { pk: number; name: string; authorization_flow: string; + verbose_name: string; + verbose_name_plural: string; constructor() { throw Error(); diff --git a/web/src/authentik.css b/web/src/authentik.css index 87e469606..c00ecdc98 100644 --- a/web/src/authentik.css +++ b/web/src/authentik.css @@ -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); diff --git a/web/src/elements/policies/BoundPoliciesList.ts b/web/src/elements/policies/BoundPoliciesList.ts index 490d11065..a9860bcdf 100644 --- a/web/src/elements/policies/BoundPoliciesList.ts +++ b/web/src/elements/policies/BoundPoliciesList.ts @@ -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 { }); } - 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[] { diff --git a/web/src/elements/table/Table.ts b/web/src/elements/table/Table.ts index 994453937..b9e38984e 100644 --- a/web/src/elements/table/Table.ts +++ b/web/src/elements/table/Table.ts @@ -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): 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): 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): TemplateResult { + return html` + `; + } + + render(table: Table): TemplateResult { + return html` + ${this.orderBy ? this.renderSortable(table) : html`${gettext(this.title)}`} + `; + } + +} + export abstract class Table extends LitElement { abstract apiEndpoint(page: number): Promise>; - abstract columns(): Array; - abstract row(item: T): Array; + 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 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 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 extends LitElement { `; } + renderSearch(): TemplateResult { + return html``; + } + renderTable(): TemplateResult { if (!this.data) { this.fetch(); } return html`
+ ${this.renderSearch()} 
${this.renderToolbar()}
@@ -143,7 +217,7 @@ export abstract class Table extends LitElement { ${this.expandable ? html`` : html``} - ${this.columns().map((col) => html`${gettext(col)}`)} + ${this.columns().map((col) => col.render(this))} ${this.data ? this.renderRows() : this.renderLoading()} diff --git a/web/src/elements/table/TablePage.ts b/web/src/elements/table/TablePage.ts index 08e28651c..70644f24d 100644 --- a/web/src/elements/table/TablePage.ts +++ b/web/src/elements/table/TablePage.ts @@ -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 extends Table { 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` { + this.search = value; + this.fetch(); + }}> + `; + } render(): TemplateResult { + const description = this.pageDescription(); return html`

${this.pageTitle()}

-

${this.pageDescription()}

+ ${description ? html`

${description}

` : html``}
diff --git a/web/src/elements/table/TableSearch.ts b/web/src/elements/table/TableSearch.ts new file mode 100644 index 000000000..1cc676c7b --- /dev/null +++ b/web/src/elements/table/TableSearch.ts @@ -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`
+
+
{ + e.preventDefault(); + if (!this.onSearch) return; + const el = this.shadowRoot?.querySelector("input[type=search]"); + if (!el) return; + if (el.value === "") return; + this.onSearch(el?.value); + }}> + { + if (!this.onSearch) return; + this.onSearch(""); +}}> + + +
+
`; + } + +} diff --git a/web/src/interfaces/AdminInterface.ts b/web/src/interfaces/AdminInterface.ts index 1b90936f5..77802216a 100644 --- a/web/src/interfaces/AdminInterface.ts +++ b/web/src/interfaces/AdminInterface.ts @@ -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_REGEX})/$` ), new SidebarItem("Sources", "/administration/sources/").activeWhen( diff --git a/web/src/pages/applications/ApplicationListPage.ts b/web/src/pages/applications/ApplicationListPage.ts index e0c3c1a51..400eb5abb 100644 --- a/web/src/pages/applications/ApplicationListPage.ts +++ b/web/src/pages/applications/ApplicationListPage.ts @@ -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 { + searchEnabled(): boolean { + return true; + } pageTitle(): string { return gettext("Applications"); } @@ -19,31 +23,51 @@ export class ApplicationList extends TablePage { return gettext("pf-icon pf-icon-applications"); } + @property() + order = "name"; + apiEndpoint(page: number): Promise> { 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` - + ${item.meta_icon ? + html`${gettext(` : + html``}`, + html` +
+ ${item.name} +
+ ${item.meta_publisher ? html`${item.meta_publisher}` : html``} +
`, + html`${item.slug}`, + html`${item.provider.name}`, + html`${item.provider.verbose_name}`, + html` + Edit
- + Delete @@ -52,4 +76,16 @@ export class ApplicationList extends TablePage { `, ]; } + + renderToolbar(): TemplateResult { + return html` + + + ${gettext("Create")} + +
+
+ ${super.renderToolbar()} + `; + } } diff --git a/web/src/pages/flows/BoundStagesList.ts b/web/src/pages/flows/BoundStagesList.ts index d1c0717e0..c575dc77b 100644 --- a/web/src/pages/flows/BoundStagesList.ts +++ b/web/src/pages/flows/BoundStagesList.ts @@ -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 { }); } - columns(): string[] { - return ["Order", "Name", "Type", ""]; + columns(): TableColumn[] { + return [ + new TableColumn("Order"), + new TableColumn("Name"), + new TableColumn("Type"), + new TableColumn(""), + ]; } row(item: FlowStageBinding): TemplateResult[] {