web: remove policy bindings page (#370)

* admin: accept ?target for PolicyBindingCreateView

* core: fix rendering of hidden fields in horizontal form

* web: add create button for application's bound policies

* admin: fix delete form not working

* web: fix ak-refresh event not being dispatched correctly

* web: fix linting errors

* admin: fix tests not loading

* build(deps-dev): bump eslint from 7.14.0 to 7.15.0 in /web (#372)

Bumps [eslint](https://github.com/eslint/eslint) from 7.14.0 to 7.15.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/master/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v7.14.0...v7.15.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump rollup from 2.34.1 to 2.34.2 in /web (#373)

Bumps [rollup](https://github.com/rollup/rollup) from 2.34.1 to 2.34.2.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v2.34.1...v2.34.2)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump @types/codemirror from 0.0.100 to 0.0.102 in /web (#374)

Bumps [@types/codemirror](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/codemirror) from 0.0.100 to 0.0.102.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/codemirror)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps-dev): bump bandit from 1.6.2 to 1.6.3 (#371)

* build(deps-dev): bump bandit from 1.6.2 to 1.6.3

Bumps [bandit](https://github.com/PyCQA/bandit) from 1.6.2 to 1.6.3.
- [Release notes](https://github.com/PyCQA/bandit/releases)
- [Commits](https://github.com/PyCQA/bandit/compare/1.6.2...1.6.3)

Signed-off-by: dependabot[bot] <support@github.com>

* root: update for new bandit version

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens.langhammer@beryju.org>

* web: add header to bound-policies

* web: fix spacing between bulk_select buttons

* web: add separate ak-bound-policies-list, add flow view page

* web: fix flows' policies not loading

* Squashed commit of the following:

commit e535cb0ec8
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Thu Dec 10 09:58:07 2020 +0100

    build(deps): bump boto3 from 1.16.32 to 1.16.33 (#383)

    Bumps [boto3](https://github.com/boto/boto3) from 1.16.32 to 1.16.33.
    - [Release notes](https://github.com/boto/boto3/releases)
    - [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
    - [Commits](https://github.com/boto/boto3/compare/1.16.32...1.16.33)

    Signed-off-by: dependabot[bot] <support@github.com>

    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit 8c1f55d3e3
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Wed Dec 9 09:06:45 2020 +0100

    build(deps): bump boto3 from 1.16.31 to 1.16.32 (#382)

    Bumps [boto3](https://github.com/boto/boto3) from 1.16.31 to 1.16.32.
    - [Release notes](https://github.com/boto/boto3/releases)
    - [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
    - [Commits](https://github.com/boto/boto3/compare/1.16.31...1.16.32)

    Signed-off-by: dependabot[bot] <support@github.com>

    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit c3a2cb44cd
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Wed Dec 9 09:06:29 2020 +0100

    build(deps): bump celery from 5.0.3 to 5.0.4 (#380)

    Bumps [celery](https://github.com/celery/celery) from 5.0.3 to 5.0.4.
    - [Release notes](https://github.com/celery/celery/releases)
    - [Changelog](https://github.com/celery/celery/blob/master/Changelog.rst)
    - [Commits](https://github.com/celery/celery/compare/v5.0.3...v5.0.4)

    Signed-off-by: dependabot[bot] <support@github.com>

    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit 682401bbf2
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Wed Dec 9 07:20:45 2020 +0100

    build(deps): bump uvicorn from 0.12.3 to 0.13.0 (#381)

    Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.12.3 to 0.13.0.
    - [Release notes](https://github.com/encode/uvicorn/releases)
    - [Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md)
    - [Commits](https://github.com/encode/uvicorn/compare/0.12.3...0.13.0)

    Signed-off-by: dependabot[bot] <support@github.com>

    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit 3e6e167348
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Tue Dec 8 10:32:00 2020 +0100

    build(deps-dev): bump @typescript-eslint/parser in /web (#377)

    Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 4.9.0 to 4.9.1.
    - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
    - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/parser/CHANGELOG.md)
    - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.9.1/packages/parser)

    Signed-off-by: dependabot[bot] <support@github.com>

    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit d08c1b7b02
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Tue Dec 8 10:31:47 2020 +0100

    build(deps): bump @sentry/browser from 5.28.0 to 5.29.0 in /web (#378)

    Bumps [@sentry/browser](https://github.com/getsentry/sentry-javascript) from 5.28.0 to 5.29.0.
    - [Release notes](https://github.com/getsentry/sentry-javascript/releases)
    - [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md)
    - [Commits](https://github.com/getsentry/sentry-javascript/compare/5.28.0...5.29.0)

    Signed-off-by: dependabot[bot] <support@github.com>

    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit 94d70d252c
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Tue Dec 8 09:02:37 2020 +0100

    build(deps): bump boto3 from 1.16.30 to 1.16.31 (#375)

    Bumps [boto3](https://github.com/boto/boto3) from 1.16.30 to 1.16.31.
    - [Release notes](https://github.com/boto/boto3/releases)
    - [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
    - [Commits](https://github.com/boto/boto3/compare/1.16.30...1.16.31)

    Signed-off-by: dependabot[bot] <support@github.com>

    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit ccfe746dd5
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Tue Dec 8 09:02:28 2020 +0100

    build(deps-dev): bump @typescript-eslint/eslint-plugin in /web (#376)

    Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 4.9.0 to 4.9.1.
    - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
    - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/CHANGELOG.md)
    - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.9.1/packages/eslint-plugin)

    Signed-off-by: dependabot[bot] <support@github.com>

    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit ef5dffa96a
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Tue Dec 8 09:02:16 2020 +0100

    build(deps): bump @sentry/tracing from 5.28.0 to 5.29.0 in /web (#379)

    Bumps [@sentry/tracing](https://github.com/getsentry/sentry-javascript) from 5.28.0 to 5.29.0.
    - [Release notes](https://github.com/getsentry/sentry-javascript/releases)
    - [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md)
    - [Commits](https://github.com/getsentry/sentry-javascript/compare/5.28.0...5.29.0)

    Signed-off-by: dependabot[bot] <support@github.com>

    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit 2caa1e7650
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Mon Dec 7 11:21:07 2020 +0100

    build(deps-dev): bump bandit from 1.6.2 to 1.6.3 (#371)

    * build(deps-dev): bump bandit from 1.6.2 to 1.6.3

    Bumps [bandit](https://github.com/PyCQA/bandit) from 1.6.2 to 1.6.3.
    - [Release notes](https://github.com/PyCQA/bandit/releases)
    - [Commits](https://github.com/PyCQA/bandit/compare/1.6.2...1.6.3)

    Signed-off-by: dependabot[bot] <support@github.com>

    * root: update for new bandit version

    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    Co-authored-by: Jens Langhammer <jens.langhammer@beryju.org>

commit 2246f3a534
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Mon Dec 7 10:26:01 2020 +0100

    build(deps): bump @types/codemirror from 0.0.100 to 0.0.102 in /web (#374)

    Bumps [@types/codemirror](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/codemirror) from 0.0.100 to 0.0.102.
    - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
    - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/codemirror)

    Signed-off-by: dependabot[bot] <support@github.com>

    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit 95ba00cb79
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Mon Dec 7 09:09:49 2020 +0100

    build(deps): bump rollup from 2.34.1 to 2.34.2 in /web (#373)

    Bumps [rollup](https://github.com/rollup/rollup) from 2.34.1 to 2.34.2.
    - [Release notes](https://github.com/rollup/rollup/releases)
    - [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
    - [Commits](https://github.com/rollup/rollup/compare/v2.34.1...v2.34.2)

    Signed-off-by: dependabot[bot] <support@github.com>

    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit 2ab4d6620f
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Mon Dec 7 09:09:24 2020 +0100

    build(deps-dev): bump eslint from 7.14.0 to 7.15.0 in /web (#372)

    Bumps [eslint](https://github.com/eslint/eslint) from 7.14.0 to 7.15.0.
    - [Release notes](https://github.com/eslint/eslint/releases)
    - [Changelog](https://github.com/eslint/eslint/blob/master/CHANGELOG.md)
    - [Commits](https://github.com/eslint/eslint/compare/v7.14.0...v7.15.0)

    Signed-off-by: dependabot[bot] <support@github.com>

    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* web: fix linting error

* web: simplify sidebar logic

* web: add support for multiple active matchers per sidebar item

* web: move router to elements

* flows: add stage_obj to flows api

* sources/*: make all sources implement SerializerModel

* web: improve listing of stages

* web: implement expandable table

* web/table: use TemplateResult as return value for row()

* web: add empty state, fix link for BoundStageList

* admin: make stage binding form accept ?target like policy binding

* web: fix styles in dark mode for expanding tables

* flows: add policybindingmodel_ptr_id to FlowStageBinding API

* web: improve wording for policies

* web: fix dark theme for tertiary buttons and static modals

* web: implement SourceViewPage

* web: add empty state for BoundPoliciesList

* web: cleanup URLs for FlowStageBindings

* root: remove url attribute from ak-messages

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
Jens L 2020-12-12 19:39:09 +01:00 committed by GitHub
parent e6a776be07
commit 488e8f769a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 896 additions and 269 deletions

View file

@ -53,10 +53,10 @@
{% for flow in object_list %} {% for flow in object_list %}
<tr role="row"> <tr role="row">
<th role="columnheader"> <th role="columnheader">
<div> <a href="/flows/{{ flow.slug }}/">
<div><code>{{ flow.slug }}</code></div> <div><code>{{ flow.slug }}</code></div>
<small>{{ flow.name }}</small> <small>{{ flow.name }}</small>
</div> </a>
</th> </th>
<td role="cell"> <td role="cell">
<span> <span>

View file

@ -63,12 +63,12 @@
{% for source in object_list %} {% for source in object_list %}
<tr role="row"> <tr role="row">
<th role="columnheader"> <th role="columnheader">
<div> <a href="/sources/{{ source.slug }}/">
<div>{{ source.name }}</div> <div>{{ source.name }}</div>
{% if not source.enabled %} {% if not source.enabled %}
<small>{% trans 'Disabled' %}</small> <small>{% trans 'Disabled' %}</small>
{% endif %} {% endif %}
</div> </a>
</th> </th>
<td role="cell"> <td role="cell">
<span> <span>

View file

View file

@ -0,0 +1,26 @@
"""admin tests"""
from django.test import TestCase
from django.test.client import RequestFactory
from authentik.admin.views.policies_bindings import PolicyBindingCreateView
from authentik.core.models import Application
class TestPolicyBindingView(TestCase):
"""Generic admin tests"""
def setUp(self):
self.factory = RequestFactory()
def test_without_get_param(self):
"""Test PolicyBindingCreateView without get params"""
request = self.factory.get("/")
view = PolicyBindingCreateView(request=request)
self.assertEqual(view.get_initial(), {})
def test_with_param(self):
"""Test PolicyBindingCreateView with get params"""
target = Application.objects.create(name="test")
request = self.factory.get("/", {"target": target.pk.hex})
view = PolicyBindingCreateView(request=request)
self.assertEqual(view.get_initial(), {"target": target, "order": 0})

View file

@ -0,0 +1,26 @@
"""admin tests"""
from django.test import TestCase
from django.test.client import RequestFactory
from authentik.admin.views.stages_bindings import StageBindingCreateView
from authentik.flows.models import Flow
class TestStageBindingView(TestCase):
"""Generic admin tests"""
def setUp(self):
self.factory = RequestFactory()
def test_without_get_param(self):
"""Test StageBindingCreateView without get params"""
request = self.factory.get("/")
view = StageBindingCreateView(request=request)
self.assertEqual(view.get_initial(), {})
def test_with_param(self):
"""Test StageBindingCreateView with get params"""
target = Flow.objects.create(name="test", slug="test")
request = self.factory.get("/", {"target": target.pk.hex})
view = StageBindingCreateView(request=request)
self.assertEqual(view.get_initial(), {"target": target, "order": 0})

View file

@ -1,10 +1,12 @@
"""authentik PolicyBinding administration""" """authentik PolicyBinding administration"""
from typing import Any
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import ( from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin, PermissionRequiredMixin as DjangoPermissionRequiredMixin,
) )
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.db.models import QuerySet from django.db.models import Max, QuerySet
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 ListView, UpdateView
@ -18,7 +20,7 @@ from authentik.admin.views.utils import (
) )
from authentik.lib.views import CreateAssignPermView from authentik.lib.views import CreateAssignPermView
from authentik.policies.forms import PolicyBindingForm from authentik.policies.forms import PolicyBindingForm
from authentik.policies.models import PolicyBinding from authentik.policies.models import PolicyBinding, PolicyBindingModel
class PolicyBindingListView( class PolicyBindingListView(
@ -67,6 +69,22 @@ class PolicyBindingCreateView(
success_url = reverse_lazy("authentik_admin:policies-bindings") success_url = reverse_lazy("authentik_admin:policies-bindings")
success_message = _("Successfully created PolicyBinding") success_message = _("Successfully created PolicyBinding")
def get_initial(self) -> dict[str, Any]:
if "target" in self.request.GET:
initial_target_pk = self.request.GET["target"]
targets = PolicyBindingModel.objects.filter(
pk=initial_target_pk
).select_subclasses()
if not targets.exists():
return {}
max_order = PolicyBinding.objects.filter(target=targets.first()).aggregate(
Max("order")
)["order__max"]
if not isinstance(max_order, int):
max_order = -1
return {"target": targets.first(), "order": max_order + 1}
return super().get_initial()
class PolicyBindingUpdateView( class PolicyBindingUpdateView(
SuccessMessageMixin, SuccessMessageMixin,

View file

@ -1,9 +1,12 @@
"""authentik StageBinding administration""" """authentik StageBinding administration"""
from typing import Any
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import ( from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin, PermissionRequiredMixin as DjangoPermissionRequiredMixin,
) )
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.db.models import Max
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 ListView, UpdateView
@ -15,7 +18,7 @@ from authentik.admin.views.utils import (
UserPaginateListMixin, UserPaginateListMixin,
) )
from authentik.flows.forms import FlowStageBindingForm from authentik.flows.forms import FlowStageBindingForm
from authentik.flows.models import FlowStageBinding from authentik.flows.models import Flow, FlowStageBinding
from authentik.lib.views import CreateAssignPermView from authentik.lib.views import CreateAssignPermView
@ -47,6 +50,20 @@ class StageBindingCreateView(
success_url = reverse_lazy("authentik_admin:stage-bindings") success_url = reverse_lazy("authentik_admin:stage-bindings")
success_message = _("Successfully created StageBinding") success_message = _("Successfully created StageBinding")
def get_initial(self) -> dict[str, Any]:
if "target" in self.request.GET:
initial_target_pk = self.request.GET["target"]
targets = Flow.objects.filter(pk=initial_target_pk).select_subclasses()
if not targets.exists():
return {}
max_order = FlowStageBinding.objects.filter(
target=targets.first()
).aggregate(Max("order"))["order__max"]
if not isinstance(max_order, int):
max_order = -1
return {"target": targets.first(), "order": max_order + 1}
return super().get_initial()
class StageBindingUpdateView( class StageBindingUpdateView(
SuccessMessageMixin, SuccessMessageMixin,

View file

@ -15,6 +15,12 @@ class SourceSerializer(ModelSerializer):
"""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("source", "") return obj._meta.object_name.lower().replace("source", "")
def to_representation(self, instance: Source):
# pyright: reportGeneralTypeIssues=false
if instance.__class__ == Source:
return super().to_representation(instance)
return instance.serializer(instance=instance).data
class Meta: class Meta:
model = Source model = Source
@ -26,6 +32,7 @@ class SourceViewSet(ReadOnlyModelViewSet):
queryset = Source.objects.all() queryset = Source.objects.all()
serializer_class = SourceSerializer serializer_class = SourceSerializer
lookup_field = "slug"
def get_queryset(self): def get_queryset(self):
return Source.objects.select_subclasses() return Source.objects.select_subclasses()

View file

@ -20,7 +20,7 @@ from authentik.core.exceptions import PropertyMappingExpressionException
from authentik.core.signals import password_changed from authentik.core.signals import password_changed
from authentik.core.types import UILoginButton from authentik.core.types import UILoginButton
from authentik.flows.models import Flow from authentik.flows.models import Flow
from authentik.lib.models import CreatedUpdatedModel from authentik.lib.models import CreatedUpdatedModel, SerializerModel
from authentik.policies.models import PolicyBindingModel from authentik.policies.models import PolicyBindingModel
LOGGER = get_logger() LOGGER = get_logger()
@ -200,7 +200,7 @@ class Application(PolicyBindingModel):
verbose_name_plural = _("Applications") verbose_name_plural = _("Applications")
class Source(PolicyBindingModel): class Source(SerializerModel, PolicyBindingModel):
"""Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server""" """Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
name = models.TextField(help_text=_("Source's display Name.")) name = models.TextField(help_text=_("Source's display Name."))

View file

@ -20,7 +20,7 @@
<div class="pf-l-stack__item"> <div class="pf-l-stack__item">
<div class="pf-c-card"> <div class="pf-c-card">
<div class="pf-c-card__body"> <div class="pf-c-card__body">
<form action="" method="post" class="pf-c-form"> <form id="delete-form" action="" method="post" class="pf-c-form">
{% csrf_token %} {% csrf_token %}
<p> <p>
{% blocktrans with object_type=object|verbose_name name=object %} {% blocktrans with object_type=object|verbose_name name=object %}
@ -35,7 +35,7 @@
</div> </div>
</section> </section>
<footer class="pf-c-modal-box__footer"> <footer class="pf-c-modal-box__footer">
<input class="pf-c-button pf-m-danger" type="submit" value="{% trans 'Delete' %}" /> <input class="pf-c-button pf-m-danger" type="submit" form="delete-form" value="{% trans 'Delete' %}" />
<a class="pf-c-button pf-m-secondary" href="{% back %}">{% trans "Back" %}</a> <a class="pf-c-button pf-m-secondary" href="{% back %}">{% trans "Back" %}</a>
</footer> </footer>
{% endblock %} {% endblock %}

View file

@ -1,7 +1,7 @@
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
<ak-messages url="{% url 'authentik_api:messages-list' %}"></ak-messages> <ak-messages></ak-messages>
<header class="pf-c-login__main-header"> <header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl"> <h1 class="pf-c-title pf-m-3xl">

View file

@ -28,7 +28,7 @@
</filter> </filter>
</svg> </svg>
</div> </div>
<ak-messages url="{% url 'authentik_api:messages-list' %}"></ak-messages> <ak-messages></ak-messages>
<div class="pf-c-login"> <div class="pf-c-login">
<div class="pf-c-login__container"> <div class="pf-c-login__container">
<header class="pf-c-login__header"> <header class="pf-c-login__header">

View file

@ -3,6 +3,9 @@
{% csrf_token %} {% csrf_token %}
{% for field in form %} {% for field in form %}
{% if field.field.widget|fieldtype == 'HiddenInput' %}
{{ field }}
{% else %}
<div class="pf-c-form__group {% if field.errors %} has-error {% endif %}"> <div class="pf-c-form__group {% if field.errors %} has-error {% endif %}">
{% if field.field.widget|fieldtype == 'RadioSelect' %} {% if field.field.widget|fieldtype == 'RadioSelect' %}
<div class="pf-c-form__group-label"> <div class="pf-c-form__group-label">
@ -105,4 +108,5 @@
</p> </p>
{% endfor %} {% endfor %}
</div> </div>
{% endif %}
{% endfor %} {% endfor %}

View file

@ -21,6 +21,7 @@ class FlowSerializer(ModelSerializer):
model = Flow model = Flow
fields = [ fields = [
"pk", "pk",
"policybindingmodel_ptr_id",
"name", "name",
"slug", "slug",
"title", "title",
@ -37,31 +38,7 @@ class FlowViewSet(ModelViewSet):
queryset = Flow.objects.all() queryset = Flow.objects.all()
serializer_class = FlowSerializer serializer_class = FlowSerializer
lookup_field = "slug"
class FlowStageBindingSerializer(ModelSerializer):
"""FlowStageBinding Serializer"""
class Meta:
model = FlowStageBinding
fields = [
"pk",
"target",
"stage",
"evaluate_on_plan",
"re_evaluate_policies",
"order",
"policies",
]
class FlowStageBindingViewSet(ModelViewSet):
"""FlowStageBinding Viewset"""
queryset = FlowStageBinding.objects.all()
serializer_class = FlowStageBindingSerializer
filterset_fields = "__all__"
class StageSerializer(ModelSerializer): class StageSerializer(ModelSerializer):
@ -92,3 +69,32 @@ class StageViewSet(ReadOnlyModelViewSet):
def get_queryset(self): def get_queryset(self):
return Stage.objects.select_subclasses() return Stage.objects.select_subclasses()
class FlowStageBindingSerializer(ModelSerializer):
"""FlowStageBinding Serializer"""
stage_obj = StageSerializer(read_only=True, source="stage")
class Meta:
model = FlowStageBinding
fields = [
"pk",
"policybindingmodel_ptr_id",
"target",
"stage",
"stage_obj",
"evaluate_on_plan",
"re_evaluate_policies",
"order",
"policies",
]
class FlowStageBindingViewSet(ModelViewSet):
"""FlowStageBinding Viewset"""
queryset = FlowStageBinding.objects.all()
serializer_class = FlowStageBindingSerializer
filterset_fields = "__all__"

View file

@ -37,6 +37,11 @@ class FlowStageBindingForm(forms.ModelForm):
queryset=Stage.objects.all().select_subclasses(), to_field_name="stage_uuid" queryset=Stage.objects.all().select_subclasses(), to_field_name="stage_uuid"
) )
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if "target" in self.initial:
self.fields["target"].widget = forms.HiddenInput()
class Meta: class Meta:
model = FlowStageBinding model = FlowStageBinding

View file

@ -20,6 +20,11 @@ class PolicyBindingForm(forms.ModelForm):
queryset=Policy.objects.all().select_subclasses(), queryset=Policy.objects.all().select_subclasses(),
) )
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if "target" in self.initial:
self.fields["target"].widget = forms.HiddenInput()
class Meta: class Meta:
model = PolicyBinding model = PolicyBinding

View file

@ -18,6 +18,8 @@ class ChannelsStorage(FallbackStorage):
def _store(self, messages: list[Message], response, *args, **kwargs): def _store(self, messages: list[Message], response, *args, **kwargs):
prefix = f"user_{self.request.user.pk}_messages_" prefix = f"user_{self.request.user.pk}_messages_"
keys = cache.keys(f"{prefix}*") keys = cache.keys(f"{prefix}*")
if len(keys) < 1:
return super()._store(messages, response, *args, **kwargs)
for key in keys: for key in keys:
uid = key.replace(prefix, "") uid = key.replace(prefix, "")
for message in messages: for message in messages:
@ -30,4 +32,4 @@ class ChannelsStorage(FallbackStorage):
"message": message.message, "message": message.message,
}, },
) )
return super()._store(messages, response, *args, **kwargs) return None

View file

@ -7,6 +7,7 @@ from django.db import models
from django.forms import ModelForm from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from ldap3 import ALL, Connection, Server from ldap3 import ALL, Connection, Server
from rest_framework.serializers import Serializer
from authentik.core.models import Group, PropertyMapping, Source from authentik.core.models import Group, PropertyMapping, Source
from authentik.lib.models import DomainlessURLValidator from authentik.lib.models import DomainlessURLValidator
@ -73,6 +74,12 @@ class LDAPSource(Source):
return LDAPSourceForm return LDAPSourceForm
@property
def serializer(self) -> Type[Serializer]:
from authentik.sources.ldap.api import LDAPSourceSerializer
return LDAPSourceSerializer
def state_cache_prefix(self, suffix: str) -> str: def state_cache_prefix(self, suffix: str) -> str:
"""Key by which the ldap source status is saved""" """Key by which the ldap source status is saved"""
return f"source_ldap_{self.pk}_state_{suffix}" return f"source_ldap_{self.pk}_state_{suffix}"

View file

@ -5,6 +5,7 @@ from django.db import models
from django.forms import ModelForm from django.forms import ModelForm
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import Serializer
from authentik.core.models import Source, UserSourceConnection from authentik.core.models import Source, UserSourceConnection
from authentik.core.types import UILoginButton from authentik.core.types import UILoginButton
@ -46,6 +47,12 @@ class OAuthSource(Source):
return OAuthSourceForm return OAuthSourceForm
@property
def serializer(self) -> Type[Serializer]:
from authentik.sources.oauth.api import OAuthSourceSerializer
return OAuthSourceSerializer
@property @property
def ui_login_button(self) -> UILoginButton: def ui_login_button(self) -> UILoginButton:
return UILoginButton( return UILoginButton(

View file

@ -7,6 +7,7 @@ from django.http import HttpRequest
from django.shortcuts import reverse from django.shortcuts import reverse
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import Serializer
from authentik.core.models import Source from authentik.core.models import Source
from authentik.core.types import UILoginButton from authentik.core.types import UILoginButton
@ -143,6 +144,12 @@ class SAMLSource(Source):
return SAMLSourceForm return SAMLSourceForm
@property
def serializer(self) -> Type[Serializer]:
from authentik.sources.saml.api import SAMLSourceSerializer
return SAMLSourceSerializer
def get_issuer(self, request: HttpRequest) -> str: def get_issuer(self, request: HttpRequest) -> str:
"""Get Source's Issuer, falling back to our Metadata URL if none is set""" """Get Source's Issuer, falling back to our Metadata URL if none is set"""
if self.issuer is None: if self.issuer is None:

View file

@ -1129,7 +1129,7 @@ paths:
tags: tags:
- flows - flows
parameters: [] parameters: []
/flows/instances/{flow_uuid}/: /flows/instances/{slug}/:
get: get:
operationId: flows_instances_read operationId: flows_instances_read
description: Flow Viewset description: Flow Viewset
@ -1183,12 +1183,13 @@ paths:
tags: tags:
- flows - flows
parameters: parameters:
- name: flow_uuid - name: slug
in: path in: path
description: A UUID string identifying this Flow. description: Visible in the URL.
required: true required: true
type: string type: string
format: uuid format: slug
pattern: ^[-a-zA-Z0-9_]+$
/outposts/outposts/: /outposts/outposts/:
get: get:
operationId: outposts_outposts_list operationId: outposts_outposts_list
@ -3788,7 +3789,7 @@ paths:
tags: tags:
- sources - sources
parameters: [] parameters: []
/sources/all/{pbm_uuid}/: /sources/all/{slug}/:
get: get:
operationId: sources_all_read operationId: sources_all_read
description: Source Viewset description: Source Viewset
@ -3801,12 +3802,13 @@ paths:
tags: tags:
- sources - sources
parameters: parameters:
- name: pbm_uuid - name: slug
in: path in: path
description: A UUID string identifying this source. description: Internal source name, used in URLs.
required: true required: true
type: string type: string
format: uuid format: slug
pattern: ^[-a-zA-Z0-9_]+$
/sources/ldap/: /sources/ldap/:
get: get:
operationId: sources_ldap_list operationId: sources_ldap_list
@ -6749,6 +6751,30 @@ definitions:
description: Optional Private Key. If this is set, you can use this keypair description: Optional Private Key. If this is set, you can use this keypair
for encryption. for encryption.
type: string type: string
Stage:
title: Stage obj
description: Stage Serializer
required:
- name
type: object
properties:
pk:
title: Stage uuid
type: string
format: uuid
readOnly: true
name:
title: Name
type: string
minLength: 1
__type__:
title: 'type '
type: string
readOnly: true
verbose_name:
title: Verbose name
type: string
readOnly: true
FlowStageBinding: FlowStageBinding:
description: FlowStageBinding Serializer description: FlowStageBinding Serializer
required: required:
@ -6762,6 +6788,10 @@ definitions:
type: string type: string
format: uuid format: uuid
readOnly: true readOnly: true
policybindingmodel_ptr_id:
title: Policybindingmodel ptr id
type: string
readOnly: true
target: target:
title: Target title: Target
type: string type: string
@ -6770,6 +6800,8 @@ definitions:
title: Stage title: Stage
type: string type: string
format: uuid format: uuid
stage_obj:
$ref: '#/definitions/Stage'
evaluate_on_plan: evaluate_on_plan:
title: Evaluate on plan title: Evaluate on plan
description: Evaluate policies during the Flow planning process. Disable this description: Evaluate policies during the Flow planning process. Disable this
@ -6805,6 +6837,10 @@ definitions:
type: string type: string
format: uuid format: uuid
readOnly: true readOnly: true
policybindingmodel_ptr_id:
title: Policybindingmodel ptr id
type: string
readOnly: true
name: name:
title: Name title: Name
type: string type: string
@ -8093,29 +8129,6 @@ definitions:
\ log out manually. (Format: hours=1;minutes=2;seconds=3)." \ log out manually. (Format: hours=1;minutes=2;seconds=3)."
type: string type: string
minLength: 1 minLength: 1
Stage:
description: Stage Serializer
required:
- name
type: object
properties:
pk:
title: Stage uuid
type: string
format: uuid
readOnly: true
name:
title: Name
type: string
minLength: 1
__type__:
title: 'type '
type: string
readOnly: true
verbose_name:
title: Verbose name
type: string
readOnly: true
CaptchaStage: CaptchaStage:
description: CaptchaStage Serializer description: CaptchaStage Serializer
required: required:

76
web/src/api/flow.ts Normal file
View file

@ -0,0 +1,76 @@
import { DefaultClient, PBResponse, QueryArguments } from "./client";
export enum FlowDesignation {
Authentication = "authentication",
Authorization = "authorization",
Invalidation = "invalidation",
Enrollment = "enrollment",
Unrenollment = "unenrollment",
Recovery = "recovery",
StageConfiguration = "stage_configuration",
}
export class Flow {
pk: string;
policybindingmodel_ptr_id: string;
name: string;
slug: string;
title: string;
designation: FlowDesignation;
background: string;
stages: string[];
policies: string[];
cache_count: number;
constructor() {
throw Error();
}
static get(slug: string): Promise<Flow> {
return DefaultClient.fetch<Flow>(["flows", "instances", slug]);
}
static list(filter?: QueryArguments): Promise<PBResponse<Flow>> {
return DefaultClient.fetch<PBResponse<Flow>>(["flows", "instances"], filter);
}
}
export class Stage {
pk: string;
name: string;
__type__: string;
verbose_name: string;
constructor() {
throw Error();
}
}
export class FlowStageBinding {
pk: string;
policybindingmodel_ptr_id: string;
target: string;
stage: string;
stage_obj: Stage;
evaluate_on_plan: boolean;
re_evaluate_policies: boolean;
order: number;
policies: string[];
constructor() {
throw Error();
}
static get(slug: string): Promise<FlowStageBinding> {
return DefaultClient.fetch<FlowStageBinding>(["flows", "bindings", slug]);
}
static list(filter?: QueryArguments): Promise<PBResponse<FlowStageBinding>> {
return DefaultClient.fetch<PBResponse<FlowStageBinding>>(["flows", "bindings"], filter);
}
static adminUrl(rest: string): string {
return `/administration/stages/bindings/${rest}`;
}
}

View file

@ -1,15 +1,33 @@
import { DefaultClient, PBResponse, QueryArguments } from "./client";
export interface Policy { export interface Policy {
pk: string; pk: string;
name: string; name: string;
[key: string]: unknown; [key: string]: unknown;
} }
export interface PolicyBinding { export class PolicyBinding {
pk: string; pk: string;
policy: string, policy: string;
policy_obj: Policy; policy_obj: Policy;
target: string; target: string;
enabled: boolean; enabled: boolean;
order: number; order: number;
timeout: number; timeout: number;
constructor() {
throw Error();
}
static get(pk: string): Promise<PolicyBinding> {
return DefaultClient.fetch<PolicyBinding>(["policies", "bindings", pk]);
}
static list(filter?: QueryArguments): Promise<PBResponse<PolicyBinding>> {
return DefaultClient.fetch<PBResponse<PolicyBinding>>(["policies", "bindings"], filter);
}
static adminUrl(rest: string): string {
return `/administration/policies/bindings/${rest}`;
}
} }

22
web/src/api/source.ts Normal file
View file

@ -0,0 +1,22 @@
import { DefaultClient, PBResponse, QueryArguments } from "./client";
export class Source {
pk: string;
name: string;
slug: string;
enabled: boolean;
authentication_flow: string;
enrollment_flow: string;
constructor() {
throw Error();
}
static get(slug: string): Promise<Source> {
return DefaultClient.fetch<Source>(["sources", "all", slug]);
}
static list(filter?: QueryArguments): Promise<PBResponse<Source>> {
return DefaultClient.fetch<PBResponse<Source>>(["sources", "all"], filter);
}
}

View file

@ -137,6 +137,14 @@ 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);
} }
/* class for pagination text */
.pf-c-options-menu__toggle {
color: var(--ak-dark-foreground);
}
/* table icon used for expanding rows */
.pf-c-table__toggle-icon {
color: var(--ak-dark-foreground);
}
/* inputs */ /* inputs */
.pf-c-form-control { .pf-c-form-control {
--pf-c-form-control--BorderTopColor: var(--ak-dark-background-lighter); --pf-c-form-control--BorderTopColor: var(--ak-dark-background-lighter);
@ -151,6 +159,13 @@ select[multiple] {
background-color: var(--ak-dark-background-light); background-color: var(--ak-dark-background-light);
color: var(--ak-dark-foreground); color: var(--ak-dark-foreground);
} }
.pf-c-button.pf-m-tertiary {
--pf-c-button--after--BorderColor: var(--ak-dark-foreground-darker);
color: var(--ak-dark-foreground-darker);
}
.pf-c-button.pf-m-tertiary:hover {
--pf-c-button--after--BorderColor: var(--ak-dark-background-lighter);
}
.pf-c-form__label-text { .pf-c-form__label-text {
color: var(--ak-dark-foreground); color: var(--ak-dark-foreground);
} }
@ -162,6 +177,12 @@ select[multiple] {
color: var(--ak-dark-foreground); color: var(--ak-dark-foreground);
} }
/* modal */ /* modal */
.pf-c-modal-box__header {
background-color: var(--ak-dark-background-light);
}
.pf-c-modal-box__body {
background-color: var(--ak-dark-background-light);
}
.pf-c-modal-box__footer { .pf-c-modal-box__footer {
background-color: var(--ak-dark-background-light); background-color: var(--ak-dark-background-light);
} }

View file

@ -0,0 +1,34 @@
import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { COMMON_STYLES } from "../common/styles";
@customElement("ak-empty-state")
export class EmptyState extends LitElement {
@property({type: String})
icon = "";
@property()
header?: string;
static get styles(): CSSResult[] {
return COMMON_STYLES;
}
render(): TemplateResult {
return html`<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<i class="pf-icon ${this.icon} pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
${this.header}
</h1>
<div class="pf-c-empty-state__body">
<slot name="body"></slot>
</div>
<div class="pf-c-empty-state__primary">
<slot name="primary"></slot>
</div>
</div>
</div>`;
}
}

View file

@ -104,6 +104,7 @@ export class ModalButton extends LitElement {
this.dispatchEvent( this.dispatchEvent(
new CustomEvent("ak-refresh", { new CustomEvent("ak-refresh", {
bubbles: true, bubbles: true,
composed: true,
}) })
); );
} }

View file

@ -0,0 +1,79 @@
import { gettext } from "django";
import { customElement, html, property, TemplateResult } from "lit-element";
import { PBResponse } from "../../api/client";
import { PolicyBinding } from "../../api/policy_binding";
import { Table } from "../../elements/table/Table";
import "../../elements/Tabs";
import "../../elements/AdminLoginsChart";
import "../../elements/buttons/ModalButton";
import "../../elements/buttons/SpinnerButton";
@customElement("ak-bound-policies-list")
export class BoundPoliciesList extends Table<PolicyBinding> {
@property()
target?: string;
apiEndpoint(page: number): Promise<PBResponse<PolicyBinding>> {
return PolicyBinding.list({
target: this.target || "",
ordering: "order",
page: page,
});
}
columns(): string[] {
return ["Policy", "Enabled", "Order", "Timeout", ""];
}
row(item: PolicyBinding): TemplateResult[] {
return [
html`${item.policy_obj.name}`,
html`${item.enabled ? "Yes" : "No"}`,
html`${item.order}`,
html`${item.timeout}`,
html`
<ak-modal-button href="${PolicyBinding.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="${PolicyBinding.adminUrl(`${item.pk}/delete/`)}">
<ak-spinner-button slot="trigger" class="pf-m-danger">
Delete
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
`,
];
}
renderEmpty(): TemplateResult {
return super.renderEmpty(html`<ak-empty-state header=${gettext("No Policies bound.")} icon="pf-icon-module">
<div slot="body">
${gettext("No policies are currently bound to this object.")}
</div>
<div slot="primary">
<ak-modal-button href=${PolicyBinding.adminUrl(`create/?target=${this.target}`)}>
<ak-spinner-button slot="trigger" class="pf-m-primary">
${gettext("Bind Policy")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
</div>
</ak-empty-state>`);
}
renderToolbar(): TemplateResult {
return html`
<ak-modal-button href=${PolicyBinding.adminUrl(`create/?target=${this.target}`)}>
<ak-spinner-button slot="trigger" class="pf-m-primary">
${gettext("Bind Policy")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
${super.renderToolbar()}
`;
}
}

View file

@ -9,7 +9,7 @@ import { Route } from "./Route";
import { ROUTES } from "../../routes"; import { ROUTES } from "../../routes";
import { RouteMatch } from "./RouteMatch"; import { RouteMatch } from "./RouteMatch";
import "../generic/SiteShell"; import "../../pages/generic/SiteShell";
@customElement("ak-router-outlet") @customElement("ak-router-outlet")
export class RouterOutlet extends LitElement { export class RouterOutlet extends LitElement {

View file

@ -13,11 +13,58 @@ import { until } from "lit-html/directives/until";
import "./SidebarBrand"; import "./SidebarBrand";
import "./SidebarUser"; import "./SidebarUser";
export interface SidebarItem { export class SidebarItem {
name: string; name: string;
path?: string[]; path?: string;
children?: SidebarItem[];
condition?: () => Promise<boolean>; _children: SidebarItem[];
condition: () => Promise<boolean>;
activeMatchers: RegExp[];
constructor(name: string, path?: string) {
this.name = name;
this.path = path;
this._children = [];
this.condition = async () => true;
this.activeMatchers = [];
if (this.path) {
this.activeMatchers.push(new RegExp(`^${this.path}$`));
}
}
children(...children: SidebarItem[]): SidebarItem {
this._children = children;
return this;
}
activeWhen(...regexp: string[]): SidebarItem {
regexp.forEach(r => {
this.activeMatchers.push(new RegExp(r));
});
return this;
}
when(condition: () => Promise<boolean>): SidebarItem {
this.condition = condition;
return this;
}
hasChildren(): boolean {
return this._children.length > 0;
}
isActive(activePath: string): boolean {
if (!this.path) {
return false;
}
return this.activeMatchers.some(v => {
const match = v.exec(activePath);
if (match !== null) {
return true;
}
});
}
} }
@customElement("ak-sidebar") @customElement("ak-sidebar")
@ -78,9 +125,9 @@ export class Sidebar extends LitElement {
return html``; return html``;
} }
} }
return html` <li class="pf-c-nav__item ${item.children ? "pf-m-expandable pf-m-expanded" : ""}"> return html` <li class="pf-c-nav__item ${item.hasChildren() ? "pf-m-expandable pf-m-expanded" : ""}">
${item.path ? ${item.path ?
html`<a href="#${item.path}" class="pf-c-nav__link ${item.path.some((v) => v === this.activePath) ? "pf-m-current": ""}"> html`<a href="#${item.path}" class="pf-c-nav__link ${item.isActive(this.activePath) ? "pf-m-current": ""}">
${item.name} ${item.name}
</a>` : </a>` :
html`<a class="pf-c-nav__link" aria-expanded="true"> html`<a class="pf-c-nav__link" aria-expanded="true">
@ -91,7 +138,7 @@ export class Sidebar extends LitElement {
</a> </a>
<section class="pf-c-nav__subnav"> <section class="pf-c-nav__subnav">
<ul class="pf-c-nav__simple-list"> <ul class="pf-c-nav__simple-list">
${item.children?.map((i) => until(this.renderItem(i), html``))} ${item._children.map((i) => until(this.renderItem(i), html``))}
</ul> </ul>
</section>`} </section>`}
</li>`; </li>`;

View file

@ -2,14 +2,22 @@ import { gettext } from "django";
import { CSSResult, html, LitElement, property, TemplateResult } from "lit-element"; import { CSSResult, html, LitElement, property, TemplateResult } from "lit-element";
import { PBResponse } from "../../api/client"; import { PBResponse } from "../../api/client";
import { COMMON_STYLES } from "../../common/styles"; import { COMMON_STYLES } from "../../common/styles";
import { htmlFromString } from "../../utils";
import "./TablePagination"; import "./TablePagination";
import "../EmptyState";
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(): Array<string>;
abstract row(item: T): Array<string>; abstract row(item: T): Array<TemplateResult>;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
renderExpanded(item: T): TemplateResult {
if (this.expandable) {
throw new Error("Expandable is enabled but renderExpanded is not overridden!");
}
return html``;
}
@property({attribute: false}) @property({attribute: false})
data?: PBResponse<T>; data?: PBResponse<T>;
@ -17,6 +25,12 @@ export abstract class Table<T> extends LitElement {
@property({type: Number}) @property({type: Number})
page = 1; page = 1;
@property({type: Boolean})
expandable = false;
@property({attribute: false})
expandedRows: boolean[] = [];
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return COMMON_STYLES; return COMMON_STYLES;
} }
@ -48,7 +62,7 @@ export abstract class Table<T> extends LitElement {
<span class="pf-c-spinner__tail-ball"></span> <span class="pf-c-spinner__tail-ball"></span>
</span> </span>
</div> </div>
<h2 class="pf-c-title pf-m-lg">Loading</h2> <h2 class="pf-c-title pf-m-lg">${gettext("Loading")}</h2>
</div> </div>
</div> </div>
</div> </div>
@ -56,21 +70,59 @@ export abstract class Table<T> extends LitElement {
</tr>`; </tr>`;
} }
renderEmpty(inner?: TemplateResult): TemplateResult {
return html`<tbody role="rowgroup">
<tr role="row">
<td role="cell" colspan="8">
<div class="pf-l-bullseye">
${inner ? inner : html`<ak-empty-state header="none"></ak-empty-state>`}
</div>
</td>
</tr>
</tbody>`;
}
private renderRows(): TemplateResult[] | undefined { private renderRows(): TemplateResult[] | undefined {
if (!this.data) { if (!this.data) {
return; return;
} }
return this.data.results.map((item) => { if (this.data.pagination.count === 0) {
const fullRow = ["<tr role=\"row\">"].concat( return [this.renderEmpty()];
this.row(item).map((col) => { }
return `<td role="cell">${col}</td>`; return this.data.results.map((item: T, idx: number) => {
}) if ((this.expandedRows.length - 1) < idx) {
); this.expandedRows[idx] = false;
fullRow.push("</tr>"); }
return htmlFromString(...fullRow); return html`<tbody role="rowgroup" class="${this.expandedRows[idx] ? "pf-m-expanded" : ""}">
<tr role="row">
${this.expandable ? html`<td class="pf-c-table__toggle" role="cell">
<button class="pf-c-button pf-m-plain ${this.expandedRows[idx] ? "pf-m-expanded" : ""}" @click=${() => {
this.expandedRows[idx] = !this.expandedRows[idx];
this.requestUpdate();
}}>
<div class="pf-c-table__toggle-icon"> <i class="fas fa-angle-down" aria-hidden="true"></i> </div>
</button>
</td>` : html``}
${this.row(item).map((col) => {
return html`<td role="cell">${col}</td>`;
})}
</tr>
<tr class="pf-c-table__expandable-row ${this.expandedRows[idx] ? "pf-m-expanded" : ""}" role="row">
<td></td>
${this.renderExpanded(item)}
</tr>
</tbody>`;
}); });
} }
renderToolbar(): TemplateResult {
return html`&nbsp;<button
@click=${() => { this.fetch(); }}
class="pf-c-button pf-m-primary">
${gettext("Refresh")}
</button>`;
}
renderTable(): TemplateResult { renderTable(): TemplateResult {
if (!this.data) { if (!this.data) {
this.fetch(); this.fetch();
@ -78,12 +130,7 @@ export abstract class Table<T> extends LitElement {
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">
<div class="pf-c-toolbar__bulk-select"> <div class="pf-c-toolbar__bulk-select">
<slot name="create-button"></slot> ${this.renderToolbar()}
<button
@click=${() => {this.fetch();}}
class="pf-c-button pf-m-primary">
${gettext("Refresh")}
</button>
</div> </div>
<ak-table-pagination <ak-table-pagination
class="pf-c-toolbar__item pf-m-pagination" class="pf-c-toolbar__item pf-m-pagination"
@ -92,15 +139,14 @@ export abstract class Table<T> extends LitElement {
</ak-table-pagination> </ak-table-pagination>
</div> </div>
</div> </div>
<table class="pf-c-table pf-m-compact pf-m-grid-md"> <table class="pf-c-table pf-m-compact pf-m-grid-md pf-m-expandable">
<thead> <thead>
<tr role="row"> <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) => html`<th role="columnheader" scope="col">${gettext(col)}</th>`)}
</tr> </tr>
</thead> </thead>
<tbody role="rowgroup"> ${this.data ? this.renderRows() : this.renderLoading()}
${this.data ? this.renderRows() : this.renderLoading()}
</tbody>
</table> </table>
<div class="pf-c-pagination pf-m-bottom"> <div class="pf-c-pagination pf-m-bottom">
<ak-table-pagination <ak-table-pagination

View file

@ -39,7 +39,7 @@
<script src="/static/dist/main.js" type="module"></script> <script src="/static/dist/main.js" type="module"></script>
</head> </head>
<body> <body>
<ak-messages url="/api/v2beta/root/messages/"></ak-messages> <ak-messages></ak-messages>
<div class="pf-c-page"> <div class="pf-c-page">
<a class="pf-c-skip-to-content pf-c-button pf-m-primary" href="#main-content" <a class="pf-c-skip-to-content pf-c-button pf-m-primary" href="#main-content"
>Skip to content</a >Skip to content</a

View file

@ -1,120 +1,45 @@
import { customElement } from "lit-element"; import { customElement } from "lit-element";
import { User } from "../api/user"; import { User } from "../api/user";
import { SidebarItem } from "../elements/sidebar/Sidebar"; import { SidebarItem } from "../elements/sidebar/Sidebar";
import { SLUG_REGEX } from "../elements/router/Route";
import { Interface } from "./Interface"; import { Interface } from "./Interface";
export const SIDEBAR_ITEMS: SidebarItem[] = [ export const SIDEBAR_ITEMS: SidebarItem[] = [
{ new SidebarItem("Library", "/library/"),
name: "Library", new SidebarItem("Monitor", "/audit/audit").when((): Promise<boolean> => {
path: ["/library/"], return User.me().then(u => u.is_superuser);
}, }),
{ new SidebarItem("Administration").children(
name: "Monitor", new SidebarItem("Overview", "/administration/overview-ng/"),
path: ["/audit/audit/"], new SidebarItem("System Tasks", "/administration/tasks/"),
condition: (): Promise<boolean> => { new SidebarItem("Applications", "/administration/applications/").activeWhen(
return User.me().then(u => u.is_superuser); `^/applications/(?<slug>${SLUG_REGEX})/$`
}, ),
}, new SidebarItem("Sources", "/administration/sources/").activeWhen(
{ `^/sources/(?<slug>${SLUG_REGEX})/$`,
name: "Administration", ),
children: [ new SidebarItem("Providers", "/administration/providers/"),
{ new SidebarItem("Flows").children(
name: "Overview", new SidebarItem("Flows", "/administration/flows/").activeWhen(`^/flows/(?<slug>${SLUG_REGEX})/$`),
path: ["/administration/overview-ng/"], new SidebarItem("Stages", "/administration/stages/"),
}, new SidebarItem("Prompts", "/administration/stages/prompts/"),
{ new SidebarItem("Invitations", "/administration/stages/invitations/"),
name: "System Tasks", ),
path: ["/administration/tasks/"], new SidebarItem("User Management").children(
}, new SidebarItem("User", "/administration/users/"),
{ new SidebarItem("Groups", "/administration/groups/")
name: "Applications", ),
path: ["/administration/applications/"], new SidebarItem("Outposts").children(
}, new SidebarItem("Outposts", "/administration/outposts/"),
{ new SidebarItem("Service Connections", "/administration/outposts/service_connections/")
name: "Sources", ),
path: ["/administration/sources/"], new SidebarItem("Policies", "/administration/policies/"),
}, new SidebarItem("Property Mappings", "/administration/property-mappings"),
{ new SidebarItem("Certificates", "/administration/crypto/certificates"),
name: "Providers", new SidebarItem("Tokens", "/administration/tokens/"),
path: ["/administration/providers/"], ).when((): Promise<boolean> => {
}, return User.me().then(u => u.is_superuser);
{ })
name: "User Management",
children: [
{
name: "User",
path: ["/administration/users/"],
},
{
name: "Groups",
path: ["/administration/groups/"],
},
],
},
{
name: "Outposts",
children: [
{
name: "Outposts",
path: ["/administration/outposts/"],
},
{
name: "Service Connections",
path: ["/administration/outposts/service_connections/"],
},
],
},
{
name: "Policies",
children: [
{
name: "Policies",
path: ["/administration/policies/"],
},
{
name: "Bindings",
path: ["/administration/policies/bindings/"],
},
],
},
{
name: "Property Mappings",
path: ["/administration/property-mappings/"],
},
{
name: "Flows",
children: [
{
name: "Flows",
path: ["/administration/flows/"],
},
{
name: "Stages",
path: ["/administration/stages/"],
},
{
name: "Prompts",
path: ["/administration/stages/prompts/"],
},
{
name: "Invitations",
path: ["/administration/stages/invitations/"],
},
],
},
{
name: "Certificates",
path: ["/administration/crypto/certificates/"],
},
{
name: "Tokens",
path: ["/administration/tokens/"],
},
],
condition: (): Promise<boolean> => {
return User.me().then(u => u.is_superuser);
},
},
]; ];
@customElement("ak-interface-admin") @customElement("ak-interface-admin")

View file

@ -3,7 +3,7 @@ import { html, LitElement, TemplateResult } from "lit-element";
import { SidebarItem } from "../elements/sidebar/Sidebar"; import { SidebarItem } from "../elements/sidebar/Sidebar";
import "../elements/Messages"; import "../elements/Messages";
import "../pages/router/RouterOutlet"; import "../elements/router/RouterOutlet";
export abstract class Interface extends LitElement { export abstract class Interface extends LitElement {

View file

@ -13,18 +13,18 @@ import "./elements/sidebar/SidebarUser";
import "./elements/table/TablePagination"; import "./elements/table/TablePagination";
import "./elements/AdminLoginsChart"; import "./elements/AdminLoginsChart";
import "./elements/EmptyState";
import "./elements/cards/AggregateCard"; import "./elements/cards/AggregateCard";
import "./elements/cards/AggregatePromiseCard"; import "./elements/cards/AggregatePromiseCard";
import "./elements/CodeMirror"; import "./elements/CodeMirror";
import "./elements/Messages"; import "./elements/Messages";
import "./elements/Spinner"; import "./elements/Spinner";
import "./elements/Tabs"; import "./elements/Tabs";
import "./elements/router/RouterOutlet";
import "./pages/generic/FlowShellCard"; import "./pages/generic/FlowShellCard";
import "./pages/generic/SiteShell"; import "./pages/generic/SiteShell";
import "./pages/router/RouterOutlet";
import "./pages/admin-overview/AdminOverviewPage"; import "./pages/admin-overview/AdminOverviewPage";
import "./pages/admin-overview/TopApplicationsTable"; import "./pages/admin-overview/TopApplicationsTable";
import "./pages/applications/ApplicationListPage"; import "./pages/applications/ApplicationListPage";

View file

@ -1,9 +1,12 @@
import { gettext } from "django"; import { gettext } from "django";
import { customElement } from "lit-element"; import { customElement, html, TemplateResult } from "lit-element";
import { Application } from "../../api/application"; import { Application } from "../../api/application";
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/SpinnerButton";
@customElement("ak-application-list") @customElement("ak-application-list")
export class ApplicationList extends TablePage<Application> { export class ApplicationList extends TablePage<Application> {
pageTitle(): string { pageTitle(): string {
@ -27,13 +30,13 @@ export class ApplicationList extends TablePage<Application> {
return ["Name", "Slug", "Provider", "Provider Type", ""]; return ["Name", "Slug", "Provider", "Provider Type", ""];
} }
row(item: Application): string[] { row(item: Application): TemplateResult[] {
return [ return [
item.name, html`${item.name}`,
item.slug, html`${item.slug}`,
item.provider.toString(), html`${item.provider}`,
item.provider.toString(), html`${item.provider}`,
` html`
<ak-modal-button href="administration/policies/bindings/${item.pk}/update/"> <ak-modal-button href="administration/policies/bindings/${item.pk}/update/">
<ak-spinner-button slot="trigger" class="pf-m-secondary"> <ak-spinner-button slot="trigger" class="pf-m-secondary">
Edit Edit

View file

@ -1,54 +1,14 @@
import { gettext } from "django"; import { gettext } from "django";
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element"; import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { Application } from "../../api/application"; import { Application } from "../../api/application";
import { DefaultClient, PBResponse } from "../../api/client"; import { DefaultClient } from "../../api/client";
import { PolicyBinding } from "../../api/policy_binding";
import { COMMON_STYLES } from "../../common/styles"; import { COMMON_STYLES } from "../../common/styles";
import { Table } from "../../elements/table/Table";
import "../../elements/Tabs"; import "../../elements/Tabs";
import "../../elements/AdminLoginsChart"; import "../../elements/AdminLoginsChart";
import "../../elements/buttons/ModalButton";
@customElement("ak-bound-policies-list") import "../../elements/buttons/SpinnerButton";
export class BoundPoliciesList extends Table<PolicyBinding> { import "../../elements/policies/BoundPoliciesList";
@property()
target?: string;
apiEndpoint(page: number): Promise<PBResponse<PolicyBinding>> {
return DefaultClient.fetch<PBResponse<PolicyBinding>>(["policies", "bindings"], {
target: this.target || "",
ordering: "order",
page: page,
});
}
columns(): string[] {
return ["Policy", "Enabled", "Order", "Timeout", ""];
}
row(item: PolicyBinding): string[] {
return [
item.policy_obj.name,
item.enabled ? "Yes" : "No",
item.order.toString(),
item.timeout.toString(),
`
<ak-modal-button href="administration/policies/bindings/${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-spinner-button slot="trigger" class="pf-m-danger">
Delete
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
`,
];
}
}
@customElement("ak-application-view") @customElement("ak-application-view")
export class ApplicationViewPage extends LitElement { export class ApplicationViewPage extends LitElement {
@ -108,7 +68,13 @@ export class ApplicationViewPage extends LitElement {
</section> </section>
<div slot="page-2" data-tab-title="Policy Bindings" class="pf-c-page__main-section pf-m-no-padding-mobile"> <div slot="page-2" data-tab-title="Policy Bindings" class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-c-card"> <div class="pf-c-card">
<ak-bound-policies-list .target=${this.application.pk}></ak-bound-policies-list> <div class="pf-c-card__header">
<div class="pf-c-card__header-main">
${gettext("These policies control which users can access this application.")}
</div>
</div>
<ak-bound-policies-list .target=${this.application.pk}>
</ak-bound-policies-list>
</div> </div>
</div> </div>
</ak-tabs>`; </ak-tabs>`;

View file

@ -0,0 +1,97 @@
import { gettext } from "django";
import { customElement, html, property, TemplateResult } from "lit-element";
import { PBResponse } from "../../api/client";
import { Table } from "../../elements/table/Table";
import "../../elements/Tabs";
import "../../elements/AdminLoginsChart";
import "../../elements/buttons/ModalButton";
import "../../elements/buttons/SpinnerButton";
import "../../elements/policies/BoundPoliciesList";
import { FlowStageBinding } from "../../api/flow";
@customElement("ak-bound-stages-list")
export class BoundStagesList extends Table<FlowStageBinding> {
expandable = true;
@property()
target?: string;
apiEndpoint(page: number): Promise<PBResponse<FlowStageBinding>> {
return FlowStageBinding.list({
target: this.target || "",
ordering: "order",
page: page,
});
}
columns(): string[] {
return ["Order", "Name", "Type", ""];
}
row(item: FlowStageBinding): TemplateResult[] {
return [
html`${item.order}`,
html`${item.stage_obj.name}`,
html`${item.stage_obj.verbose_name}`,
html`
<ak-modal-button href="${FlowStageBinding.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="${FlowStageBinding.adminUrl(`${item.pk}/delete/`)}">
<ak-spinner-button slot="trigger" class="pf-m-danger">
Delete
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
`,
];
}
renderExpanded(item: FlowStageBinding): TemplateResult {
return html`
<td></td>
<td role="cell" colspan="3">
<div class="pf-c-table__expandable-row-content">
<div class="pf-c-content">
<p>${gettext("These policies control when this stage will be applied to the flow.")}</p>
<ak-bound-policies-list .target=${item.policybindingmodel_ptr_id}>
</ak-bound-policies-list>
</div>
</div>
</td>
<td></td>
<td></td>`;
}
renderEmpty(): TemplateResult {
return super.renderEmpty(html`<ak-empty-state header=${gettext("No Stages bound")} icon="pf-icon-module">
<div slot="body">
${gettext("No stages are currently bound to this flow.")}
</div>
<div slot="primary">
<ak-modal-button href="${FlowStageBinding.adminUrl(`create/?target=${this.target}`)}">
<ak-spinner-button slot="trigger" class="pf-m-primary">
${gettext("Bind Stage")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
</div>
</ak-empty-state>`);
}
renderToolbar(): TemplateResult {
return html`
<ak-modal-button href="${FlowStageBinding.adminUrl(`create/?target=${this.target}`)}">
<ak-spinner-button slot="trigger" class="pf-m-primary">
${gettext("Bind Stage")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
${super.renderToolbar()}
`;
}
}

View file

@ -0,0 +1,71 @@
import { gettext } from "django";
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { COMMON_STYLES } from "../../common/styles";
import { Flow } from "../../api/flow";
import "../../elements/Tabs";
import "../../elements/AdminLoginsChart";
import "../../elements/buttons/ModalButton";
import "../../elements/buttons/SpinnerButton";
import "../../elements/policies/BoundPoliciesList";
import "./BoundStagesList";
@customElement("ak-flow-view")
export class FlowViewPage extends LitElement {
@property()
set args(value: { [key: string]: string }) {
this.flowSlug = value.slug;
}
@property()
set flowSlug(value: string) {
Flow.get(value).then((flow) => (this.flow = flow));
}
@property({attribute: false})
flow?: Flow;
static get styles(): CSSResult[] {
return COMMON_STYLES.concat(
css`
img.pf-icon {
max-height: 24px;
}
`
);
}
render(): TemplateResult {
if (!this.flow) {
return html``;
}
return html`<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1>
<i class="pf-icon pf-icon-process-automation"></i>
${this.flow?.name}
</h1>
<p>${this.flow?.title}</p>
</div>
</section>
<ak-tabs>
<div slot="page-2" data-tab-title="${gettext("Stage Bindings")}" class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-c-card">
<ak-bound-stages-list .target=${this.flow.pk}>
</ak-bound-stages-list>
</div>
</div>
<div slot="page-3" data-tab-title="${gettext("Policy Bindings")}" class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-c-card">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
${gettext("These policies control which users can access this flow.")}
</div>
</div>
<ak-bound-policies-list .target=${this.flow.policybindingmodel_ptr_id}>
</ak-bound-policies-list>
</div>
</div>
</ak-tabs>`;
}
}

View file

@ -0,0 +1,63 @@
import { gettext } from "django";
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { COMMON_STYLES } from "../../common/styles";
import "../../elements/Tabs";
import "../../elements/AdminLoginsChart";
import "../../elements/buttons/ModalButton";
import "../../elements/buttons/SpinnerButton";
import "../../elements/policies/BoundPoliciesList";
import { Source } from "../../api/source";
@customElement("ak-source-view")
export class SourceViewPage extends LitElement {
@property()
set args(value: { [key: string]: string }) {
this.sourceSlug = value.slug;
}
@property()
set sourceSlug(value: string) {
Source.get(value).then((source) => (this.source = source));
}
@property({attribute: false})
source?: Source;
static get styles(): CSSResult[] {
return COMMON_STYLES.concat(
css`
img.pf-icon {
max-height: 24px;
}
`
);
}
render(): TemplateResult {
if (!this.source) {
return html``;
}
return html`<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1>
<i class="pf-icon pf-icon-middleware"></i>
${this.source?.name}
</h1>
</div>
</section>
<ak-tabs>
<div slot="page-2" data-tab-title="Policy Bindings" class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-c-card">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
${gettext("These policies control which users can access this application.")}
</div>
</div>
<ak-bound-policies-list .target=${this.source.pk}>
</ak-bound-policies-list>
</div>
</div>
</ak-tabs>`;
}
}

View file

@ -1,10 +1,12 @@
import { html } from "lit-html"; import { html } from "lit-html";
import { Route, SLUG_REGEX } from "./pages/router/Route"; import { Route, SLUG_REGEX } from "./elements/router/Route";
import "./pages/LibraryPage"; import "./pages/LibraryPage";
import "./pages/admin-overview/AdminOverviewPage"; import "./pages/admin-overview/AdminOverviewPage";
import "./pages/applications/ApplicationListPage"; import "./pages/applications/ApplicationListPage";
import "./pages/applications/ApplicationViewPage"; import "./pages/applications/ApplicationViewPage";
import "./pages/sources/SourceViewPage";
import "./pages/flows/FlowViewPage";
export const ROUTES: Route[] = [ export const ROUTES: Route[] = [
// Prevent infinite Shell loops // Prevent infinite Shell loops
@ -16,4 +18,10 @@ export const ROUTES: Route[] = [
new Route(new RegExp(`^/applications/(?<slug>${SLUG_REGEX})/$`)).then((args) => { new Route(new RegExp(`^/applications/(?<slug>${SLUG_REGEX})/$`)).then((args) => {
return html`<ak-application-view .args=${args}></ak-application-view>`; return html`<ak-application-view .args=${args}></ak-application-view>`;
}), }),
new Route(new RegExp(`^/sources/(?<slug>${SLUG_REGEX})/$`)).then((args) => {
return html`<ak-source-view .args=${args}></ak-source-view>`;
}),
new Route(new RegExp(`^/flows/(?<slug>${SLUG_REGEX})/$`)).then((args) => {
return html`<ak-flow-view .args=${args}></ak-flow-view>`;
}),
]; ];