initial interfaces
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
parent
bb92c4a967
commit
e39c460e3a
|
@ -33,6 +33,7 @@ from authentik.flows.api.flows import FlowViewSet
|
||||||
from authentik.flows.api.stages import StageViewSet
|
from authentik.flows.api.stages import StageViewSet
|
||||||
from authentik.flows.views.executor import FlowExecutorView
|
from authentik.flows.views.executor import FlowExecutorView
|
||||||
from authentik.flows.views.inspector import FlowInspectorView
|
from authentik.flows.views.inspector import FlowInspectorView
|
||||||
|
from authentik.interfaces.api import InterfaceViewSet
|
||||||
from authentik.outposts.api.outposts import OutpostViewSet
|
from authentik.outposts.api.outposts import OutpostViewSet
|
||||||
from authentik.outposts.api.service_connections import (
|
from authentik.outposts.api.service_connections import (
|
||||||
DockerServiceConnectionViewSet,
|
DockerServiceConnectionViewSet,
|
||||||
|
@ -123,6 +124,8 @@ router.register("core/user_consent", UserConsentViewSet)
|
||||||
router.register("core/tokens", TokenViewSet)
|
router.register("core/tokens", TokenViewSet)
|
||||||
router.register("core/tenants", TenantViewSet)
|
router.register("core/tenants", TenantViewSet)
|
||||||
|
|
||||||
|
router.register("interfaces", InterfaceViewSet)
|
||||||
|
|
||||||
router.register("outposts/instances", OutpostViewSet)
|
router.register("outposts/instances", OutpostViewSet)
|
||||||
router.register("outposts/service_connections/all", ServiceConnectionViewSet)
|
router.register("outposts/service_connections/all", ServiceConnectionViewSet)
|
||||||
router.register("outposts/service_connections/docker", DockerServiceConnectionViewSet)
|
router.register("outposts/service_connections/docker", DockerServiceConnectionViewSet)
|
||||||
|
|
|
@ -6,14 +6,18 @@ from django.contrib.auth.decorators import login_required
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||||
from django.views.generic import RedirectView
|
from django.views.generic import RedirectView
|
||||||
|
from django.http import HttpRequest, HttpResponse
|
||||||
from authentik.core.views import apps, impersonate
|
from authentik.core.views import apps, impersonate
|
||||||
from authentik.core.views.debug import AccessDeniedView
|
from authentik.core.views.debug import AccessDeniedView
|
||||||
from authentik.core.views.interface import FlowInterfaceView, InterfaceView
|
|
||||||
from authentik.core.views.session import EndSessionView
|
from authentik.core.views.session import EndSessionView
|
||||||
from authentik.root.asgi_middleware import SessionMiddleware
|
from authentik.root.asgi_middleware import SessionMiddleware
|
||||||
from authentik.root.messages.consumer import MessageConsumer
|
from authentik.root.messages.consumer import MessageConsumer
|
||||||
|
|
||||||
|
|
||||||
|
def placeholder_view(request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
|
return HttpResponse(status_code=200)
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path(
|
path(
|
||||||
"",
|
"",
|
||||||
|
@ -40,31 +44,16 @@ urlpatterns = [
|
||||||
name="impersonate-end",
|
name="impersonate-end",
|
||||||
),
|
),
|
||||||
# Interfaces
|
# Interfaces
|
||||||
path(
|
|
||||||
"if/admin/",
|
|
||||||
ensure_csrf_cookie(InterfaceView.as_view(template_name="if/admin.html")),
|
|
||||||
name="if-admin",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"if/user/",
|
|
||||||
ensure_csrf_cookie(InterfaceView.as_view(template_name="if/user.html")),
|
|
||||||
name="if-user",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"if/flow/<slug:flow_slug>/",
|
|
||||||
ensure_csrf_cookie(FlowInterfaceView.as_view()),
|
|
||||||
name="if-flow",
|
|
||||||
),
|
|
||||||
path(
|
path(
|
||||||
"if/session-end/<slug:application_slug>/",
|
"if/session-end/<slug:application_slug>/",
|
||||||
ensure_csrf_cookie(EndSessionView.as_view()),
|
ensure_csrf_cookie(EndSessionView.as_view()),
|
||||||
name="if-session-end",
|
name="if-session-end",
|
||||||
),
|
),
|
||||||
# Fallback for WS
|
# Fallback for WS
|
||||||
path("ws/outpost/<uuid:pk>/", InterfaceView.as_view(template_name="if/admin.html")),
|
path("ws/outpost/<uuid:pk>/", placeholder_view),
|
||||||
path(
|
path(
|
||||||
"ws/client/",
|
"ws/client/",
|
||||||
InterfaceView.as_view(template_name="if/admin.html"),
|
placeholder_view,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
"""Interface views"""
|
|
||||||
from json import dumps
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
from django.views.generic.base import TemplateView
|
|
||||||
from rest_framework.request import Request
|
|
||||||
|
|
||||||
from authentik import get_build_hash
|
|
||||||
from authentik.admin.tasks import LOCAL_VERSION
|
|
||||||
from authentik.api.v3.config import ConfigView
|
|
||||||
from authentik.flows.models import Flow
|
|
||||||
from authentik.tenants.api import CurrentTenantSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class InterfaceView(TemplateView):
|
|
||||||
"""Base interface view"""
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
|
||||||
kwargs["config_json"] = dumps(ConfigView(request=Request(self.request)).get_config().data)
|
|
||||||
kwargs["tenant_json"] = dumps(CurrentTenantSerializer(self.request.tenant).data)
|
|
||||||
kwargs["version_family"] = f"{LOCAL_VERSION.major}.{LOCAL_VERSION.minor}"
|
|
||||||
kwargs["version_subdomain"] = f"version-{LOCAL_VERSION.major}-{LOCAL_VERSION.minor}"
|
|
||||||
kwargs["build"] = get_build_hash()
|
|
||||||
return super().get_context_data(**kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class FlowInterfaceView(InterfaceView):
|
|
||||||
"""Flow interface"""
|
|
||||||
|
|
||||||
template_name = "if/flow.html"
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
|
||||||
kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
|
|
||||||
kwargs["inspector"] = "inspector" in self.request.GET
|
|
||||||
return super().get_context_data(**kwargs)
|
|
0
authentik/interfaces/__init__.py
Normal file
0
authentik/interfaces/__init__.py
Normal file
20
authentik/interfaces/api.py
Normal file
20
authentik/interfaces/api.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
from rest_framework.serializers import ModelSerializer
|
||||||
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.interfaces.models import Interface
|
||||||
|
|
||||||
|
|
||||||
|
class InterfaceSerializer(ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Interface
|
||||||
|
fields = [
|
||||||
|
"interface_uuid",
|
||||||
|
"url_name",
|
||||||
|
"type",
|
||||||
|
"template",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class InterfaceViewSet(ModelViewSet):
|
||||||
|
queryset = Interface.objects.all()
|
||||||
|
serializer_class = InterfaceSerializer
|
12
authentik/interfaces/apps.py
Normal file
12
authentik/interfaces/apps.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
"""authentik interfaces app config"""
|
||||||
|
from authentik.blueprints.apps import ManagedAppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AuthentikInterfacesConfig(ManagedAppConfig):
|
||||||
|
"""authentik interfaces app config"""
|
||||||
|
|
||||||
|
name = "authentik.interfaces"
|
||||||
|
label = "authentik_interfaces"
|
||||||
|
verbose_name = "authentik Interfaces"
|
||||||
|
mountpoint = "if/"
|
||||||
|
default = True
|
36
authentik/interfaces/migrations/0001_initial.py
Normal file
36
authentik/interfaces/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
# Generated by Django 4.1.7 on 2023-02-16 11:01
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Interface",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"interface_uuid",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("url_name", models.SlugField()),
|
||||||
|
(
|
||||||
|
"type",
|
||||||
|
models.TextField(
|
||||||
|
choices=[("user", "User"), ("admin", "Admin"), ("flow", "Flow")]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("template", models.TextField()),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
0
authentik/interfaces/migrations/__init__.py
Normal file
0
authentik/interfaces/migrations/__init__.py
Normal file
33
authentik/interfaces/models.py
Normal file
33
authentik/interfaces/models.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
"""Interface models"""
|
||||||
|
from typing import Type
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from rest_framework.serializers import BaseSerializer
|
||||||
|
|
||||||
|
from authentik.lib.models import SerializerModel
|
||||||
|
|
||||||
|
|
||||||
|
class InterfaceType(models.TextChoices):
|
||||||
|
"""Interface types"""
|
||||||
|
|
||||||
|
USER = "user"
|
||||||
|
ADMIN = "admin"
|
||||||
|
FLOW = "flow"
|
||||||
|
|
||||||
|
|
||||||
|
class Interface(SerializerModel):
|
||||||
|
"""Interface"""
|
||||||
|
|
||||||
|
interface_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
||||||
|
|
||||||
|
url_name = models.SlugField()
|
||||||
|
|
||||||
|
type = models.TextField(choices=InterfaceType.choices)
|
||||||
|
template = models.TextField()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def serializer(self) -> Type[BaseSerializer]:
|
||||||
|
from authentik.interfaces.api import InterfaceSerializer
|
||||||
|
|
||||||
|
return InterfaceSerializer
|
16
authentik/interfaces/urls.py
Normal file
16
authentik/interfaces/urls.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
"""Interface urls"""
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from authentik.interfaces.views import InterfaceView
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"<slug:if_name>/",
|
||||||
|
InterfaceView.as_view(),
|
||||||
|
kwargs={"flow_slug": None},
|
||||||
|
name="if",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"<slug:if_name>/<slug:flow_slug>/", InterfaceView.as_view(), name="if"
|
||||||
|
),
|
||||||
|
]
|
64
authentik/interfaces/views.py
Normal file
64
authentik/interfaces/views.py
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
"""Interface views"""
|
||||||
|
from json import dumps
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from django.http import Http404, HttpRequest, HttpResponse
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.template import Template, TemplateSyntaxError, engines
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.views import View
|
||||||
|
from rest_framework.request import Request
|
||||||
|
from django.views.decorators.cache import cache_page
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||||
|
|
||||||
|
from authentik import get_build_hash
|
||||||
|
from authentik.admin.tasks import LOCAL_VERSION
|
||||||
|
from authentik.api.v3.config import ConfigView
|
||||||
|
from authentik.flows.models import Flow
|
||||||
|
from authentik.interfaces.models import Interface, InterfaceType
|
||||||
|
from authentik.tenants.api import CurrentTenantSerializer
|
||||||
|
|
||||||
|
|
||||||
|
def template_from_string(template_string: str) -> Template:
|
||||||
|
"""Render template from string"""
|
||||||
|
chain = []
|
||||||
|
engine_list = engines.all()
|
||||||
|
for engine in engine_list:
|
||||||
|
try:
|
||||||
|
return engine.from_string(template_string)
|
||||||
|
except TemplateSyntaxError as exc:
|
||||||
|
chain.append(exc)
|
||||||
|
raise TemplateSyntaxError(template_string, chain=chain)
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(ensure_csrf_cookie, name="dispatch")
|
||||||
|
@method_decorator(cache_page(60 * 10), name="dispatch")
|
||||||
|
class InterfaceView(View):
|
||||||
|
"""General interface view"""
|
||||||
|
|
||||||
|
def get_context_data(self) -> dict[str, Any]:
|
||||||
|
"""Get template context"""
|
||||||
|
return {
|
||||||
|
"config_json": dumps(ConfigView(request=Request(self.request)).get_config().data),
|
||||||
|
"tenant_json": dumps(CurrentTenantSerializer(self.request.tenant).data),
|
||||||
|
"version_family": f"{LOCAL_VERSION.major}.{LOCAL_VERSION.minor}",
|
||||||
|
"version_subdomain": f"version-{LOCAL_VERSION.major}-{LOCAL_VERSION.minor}",
|
||||||
|
"build": get_build_hash(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def type_flow(self, context: dict[str, Any]):
|
||||||
|
"""Special handling for flow interfaces"""
|
||||||
|
if self.kwargs.get("flow_slug", None) is None:
|
||||||
|
raise Http404()
|
||||||
|
context["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
|
||||||
|
context["inspector"] = "inspector" in self.request.GET
|
||||||
|
|
||||||
|
def dispatch(self, request: HttpRequest, if_name: str, **kwargs: Any) -> HttpResponse:
|
||||||
|
context = self.get_context_data()
|
||||||
|
# TODO: Cache
|
||||||
|
interface: Interface = get_object_or_404(Interface, url_name=if_name)
|
||||||
|
if interface.type == InterfaceType.FLOW:
|
||||||
|
self.type_flow(context)
|
||||||
|
template = template_from_string(interface.template)
|
||||||
|
return TemplateResponse(request, template, context)
|
|
@ -65,6 +65,7 @@ INSTALLED_APPS = [
|
||||||
"authentik.admin",
|
"authentik.admin",
|
||||||
"authentik.api",
|
"authentik.api",
|
||||||
"authentik.crypto",
|
"authentik.crypto",
|
||||||
|
"authentik.interfaces",
|
||||||
"authentik.events",
|
"authentik.events",
|
||||||
"authentik.flows",
|
"authentik.flows",
|
||||||
"authentik.lib",
|
"authentik.lib",
|
||||||
|
|
|
@ -61,6 +61,7 @@
|
||||||
"authentik_events.notificationwebhookmapping",
|
"authentik_events.notificationwebhookmapping",
|
||||||
"authentik_flows.flow",
|
"authentik_flows.flow",
|
||||||
"authentik_flows.flowstagebinding",
|
"authentik_flows.flowstagebinding",
|
||||||
|
"authentik_interfaces.interface",
|
||||||
"authentik_outposts.dockerserviceconnection",
|
"authentik_outposts.dockerserviceconnection",
|
||||||
"authentik_outposts.kubernetesserviceconnection",
|
"authentik_outposts.kubernetesserviceconnection",
|
||||||
"authentik_outposts.outpost",
|
"authentik_outposts.outpost",
|
||||||
|
|
139
blueprints/system/interfaces.yaml
Normal file
139
blueprints/system/interfaces.yaml
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
version: 1
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
blueprints.goauthentik.io/system: "true"
|
||||||
|
name: System - Interfaces
|
||||||
|
entries:
|
||||||
|
- model: authentik_interfaces.interface
|
||||||
|
identifiers:
|
||||||
|
url_name: user
|
||||||
|
type: user
|
||||||
|
attrs:
|
||||||
|
template: |
|
||||||
|
{% extends "base/skeleton.html" %}
|
||||||
|
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<script src="{% static 'dist/user/UserInterface.js' %}?version={{ version }}" type="module"></script>
|
||||||
|
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: light)">
|
||||||
|
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: dark)">
|
||||||
|
<link rel="icon" href="{{ tenant.branding_favicon }}">
|
||||||
|
<link rel="shortcut icon" href="{{ tenant.branding_favicon }}">
|
||||||
|
{% include "base/header_js.html" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<ak-message-container></ak-message-container>
|
||||||
|
<ak-interface-user>
|
||||||
|
<section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
|
||||||
|
<div class="pf-c-empty-state" style="height: 100vh;">
|
||||||
|
<div class="pf-c-empty-state__content">
|
||||||
|
<span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="{% trans 'Loading...' %}">
|
||||||
|
<span class="pf-c-spinner__clipper"></span>
|
||||||
|
<span class="pf-c-spinner__lead-ball"></span>
|
||||||
|
<span class="pf-c-spinner__tail-ball"></span>
|
||||||
|
</span>
|
||||||
|
<h1 class="pf-c-title pf-m-lg">
|
||||||
|
{% trans "Loading..." %}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</ak-interface-user>
|
||||||
|
{% endblock %}
|
||||||
|
- model: authentik_interfaces.interface
|
||||||
|
identifiers:
|
||||||
|
url_name: admin
|
||||||
|
type: admin
|
||||||
|
attrs:
|
||||||
|
template: |
|
||||||
|
{% extends "base/skeleton.html" %}
|
||||||
|
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<script src="{% static 'dist/admin/AdminInterface.js' %}?version={{ version }}" type="module"></script>
|
||||||
|
<meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)">
|
||||||
|
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
|
||||||
|
<link rel="icon" href="{{ tenant.branding_favicon }}">
|
||||||
|
<link rel="shortcut icon" href="{{ tenant.branding_favicon }}">
|
||||||
|
{% include "base/header_js.html" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<ak-message-container></ak-message-container>
|
||||||
|
<ak-interface-admin>
|
||||||
|
<section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
|
||||||
|
<div class="pf-c-empty-state" style="height: 100vh;">
|
||||||
|
<div class="pf-c-empty-state__content">
|
||||||
|
<span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="{% trans 'Loading...' %}">
|
||||||
|
<span class="pf-c-spinner__clipper"></span>
|
||||||
|
<span class="pf-c-spinner__lead-ball"></span>
|
||||||
|
<span class="pf-c-spinner__tail-ball"></span>
|
||||||
|
</span>
|
||||||
|
<h1 class="pf-c-title pf-m-lg">
|
||||||
|
{% trans "Loading..." %}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</ak-interface-admin>
|
||||||
|
{% endblock %}
|
||||||
|
- model: authentik_interfaces.interface
|
||||||
|
identifiers:
|
||||||
|
url_name: flow
|
||||||
|
type: flow
|
||||||
|
attrs:
|
||||||
|
template: |
|
||||||
|
{% extends "base/skeleton.html" %}
|
||||||
|
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block head_before %}
|
||||||
|
{{ block.super }}
|
||||||
|
<link rel="prefetch" href="{{ flow.background_url }}" />
|
||||||
|
<link rel="icon" href="{{ tenant.branding_favicon }}">
|
||||||
|
<link rel="shortcut icon" href="{{ tenant.branding_favicon }}">
|
||||||
|
{% if flow.compatibility_mode and not inspector %}
|
||||||
|
<script>ShadyDOM = { force: !navigator.webdriver };</script>
|
||||||
|
{% endif %}
|
||||||
|
{% include "base/header_js.html" %}
|
||||||
|
<script>
|
||||||
|
window.authentik.flow = {
|
||||||
|
"layout": "{{ flow.layout }}",
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<script src="{% static 'dist/flow/FlowInterface.js' %}?version={{ version }}" type="module"></script>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--ak-flow-background: url("{{ flow.background_url }}");
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<ak-message-container></ak-message-container>
|
||||||
|
<ak-flow-executor>
|
||||||
|
<section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
|
||||||
|
<div class="pf-c-empty-state" style="height: 100vh;">
|
||||||
|
<div class="pf-c-empty-state__content">
|
||||||
|
<span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="{% trans 'Loading...' %}">
|
||||||
|
<span class="pf-c-spinner__clipper"></span>
|
||||||
|
<span class="pf-c-spinner__lead-ball"></span>
|
||||||
|
<span class="pf-c-spinner__tail-ball"></span>
|
||||||
|
</span>
|
||||||
|
<h1 class="pf-c-title pf-m-lg">
|
||||||
|
{% trans "Loading..." %}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</ak-flow-executor>
|
||||||
|
{% endblock %}
|
Reference in a new issue