Compare commits

..

1 Commits

Author SHA1 Message Date
b5e0577569 Fixes indentation on text block 2025-05-08 12:11:30 +01:00
157 changed files with 3963 additions and 3580 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 2025.4.1 current_version = 2025.4.0
tag = True tag = True
commit = True commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))? parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?

View File

@ -200,7 +200,7 @@ jobs:
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: web/dist path: web/dist
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'web/src/**', 'web/packages/sfe/src/**') }}-b key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'web/src/**') }}
- name: prepare web ui - name: prepare web ui
if: steps.cache-web.outputs.cache-hit != 'true' if: steps.cache-web.outputs.cache-hit != 'true'
working-directory: web working-directory: web
@ -208,7 +208,6 @@ jobs:
npm ci npm ci
make -C .. gen-client-ts make -C .. gen-client-ts
npm run build npm run build
npm run build:sfe
- name: run e2e - name: run e2e
run: | run: |
uv run coverage run manage.py test ${{ matrix.job.glob }} uv run coverage run manage.py test ${{ matrix.job.glob }}

View File

@ -40,8 +40,7 @@ COPY ./web /work/web/
COPY ./website /work/website/ COPY ./website /work/website/
COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api
RUN npm run build && \ RUN npm run build
npm run build:sfe
# Stage 3: Build go proxy # Stage 3: Build go proxy
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS go-builder FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS go-builder
@ -94,7 +93,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
/bin/sh -c "GEOIPUPDATE_LICENSE_KEY_FILE=/run/secrets/GEOIPUPDATE_LICENSE_KEY /usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0" /bin/sh -c "GEOIPUPDATE_LICENSE_KEY_FILE=/run/secrets/GEOIPUPDATE_LICENSE_KEY /usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
# Stage 5: Download uv # Stage 5: Download uv
FROM ghcr.io/astral-sh/uv:0.7.4 AS uv FROM ghcr.io/astral-sh/uv:0.7.2 AS uv
# Stage 6: Base python image # Stage 6: Base python image
FROM ghcr.io/goauthentik/fips-python:3.13.3-slim-bookworm-fips AS python-base FROM ghcr.io/goauthentik/fips-python:3.13.3-slim-bookworm-fips AS python-base

View File

@ -2,7 +2,7 @@
from os import environ from os import environ
__version__ = "2025.4.1" __version__ = "2025.4.0"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -5,10 +5,10 @@ from typing import Any
from django.db.models import F, Q from django.db.models import F, Q
from django.db.models import Value as V from django.db.models import Value as V
from django.http.request import HttpRequest from django.http.request import HttpRequest
from sentry_sdk import get_current_span
from authentik import get_full_version from authentik import get_full_version
from authentik.brands.models import Brand from authentik.brands.models import Brand
from authentik.lib.sentry import get_http_meta
from authentik.tenants.models import Tenant from authentik.tenants.models import Tenant
_q_default = Q(default=True) _q_default = Q(default=True)
@ -32,9 +32,13 @@ def context_processor(request: HttpRequest) -> dict[str, Any]:
"""Context Processor that injects brand object into every template""" """Context Processor that injects brand object into every template"""
brand = getattr(request, "brand", DEFAULT_BRAND) brand = getattr(request, "brand", DEFAULT_BRAND)
tenant = getattr(request, "tenant", Tenant()) tenant = getattr(request, "tenant", Tenant())
trace = ""
span = get_current_span()
if span:
trace = span.to_traceparent()
return { return {
"brand": brand, "brand": brand,
"footer_links": tenant.footer_links, "footer_links": tenant.footer_links,
"html_meta": {**get_http_meta()}, "sentry_trace": trace,
"version": get_full_version(), "version": get_full_version(),
} }

View File

@ -99,17 +99,18 @@ class GroupSerializer(ModelSerializer):
if superuser if superuser
else "authentik_core.disable_group_superuser" else "authentik_core.disable_group_superuser"
) )
if self.instance or superuser: has_perm = user.has_perm(perm)
has_perm = user.has_perm(perm) or user.has_perm(perm, self.instance) if self.instance and not has_perm:
if not has_perm: has_perm = user.has_perm(perm, self.instance)
raise ValidationError( if not has_perm:
_( raise ValidationError(
( _(
"User does not have permission to set " (
"superuser status to {superuser_status}." "User does not have permission to set "
).format_map({"superuser_status": superuser}) "superuser status to {superuser_status}."
) ).format_map({"superuser_status": superuser})
) )
)
return superuser return superuser
class Meta: class Meta:

View File

@ -2,7 +2,6 @@
from django.apps import apps from django.apps import apps
from django.contrib.auth.management import create_permissions from django.contrib.auth.management import create_permissions
from django.core.management import call_command
from django.core.management.base import BaseCommand, no_translations from django.core.management.base import BaseCommand, no_translations
from guardian.management import create_anonymous_user from guardian.management import create_anonymous_user
@ -17,10 +16,6 @@ class Command(BaseCommand):
"""Check permissions for all apps""" """Check permissions for all apps"""
for tenant in Tenant.objects.filter(ready=True): for tenant in Tenant.objects.filter(ready=True):
with tenant: with tenant:
# See https://code.djangoproject.com/ticket/28417
# Remove potential lingering old permissions
call_command("remove_stale_contenttypes", "--no-input")
for app in apps.get_app_configs(): for app in apps.get_app_configs():
self.stdout.write(f"Checking app {app.name} ({app.label})\n") self.stdout.write(f"Checking app {app.name} ({app.label})\n")
create_permissions(app, verbosity=0) create_permissions(app, verbosity=0)

View File

@ -31,10 +31,7 @@ class PickleSerializer:
def loads(self, data): def loads(self, data):
"""Unpickle data to be loaded from redis""" """Unpickle data to be loaded from redis"""
try: return pickle.loads(data) # nosec
return pickle.loads(data) # nosec
except Exception:
return {}
def _migrate_session( def _migrate_session(

View File

@ -1,27 +0,0 @@
# Generated by Django 5.1.9 on 2025-05-14 11:15
from django.apps.registry import Apps
from django.db import migrations
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def remove_old_authenticated_session_content_type(
apps: Apps, schema_editor: BaseDatabaseSchemaEditor
):
db_alias = schema_editor.connection.alias
ContentType = apps.get_model("contenttypes", "ContentType")
ContentType.objects.using(db_alias).filter(model="oldauthenticatedsession").delete()
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0047_delete_oldauthenticatedsession"),
]
operations = [
migrations.RunPython(
code=remove_old_authenticated_session_content_type,
),
]

View File

@ -21,9 +21,7 @@
<script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script> <script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script>
{% block head %} {% block head %}
{% endblock %} {% endblock %}
{% for key, value in html_meta.items %} <meta name="sentry-trace" content="{{ sentry_trace }}" />
<meta name="{{key}}" content="{{ value }}" />
{% endfor %}
</head> </head>
<body> <body>
{% block body %} {% block body %}

View File

@ -124,16 +124,6 @@ class TestGroupsAPI(APITestCase):
{"is_superuser": ["User does not have permission to set superuser status to True."]}, {"is_superuser": ["User does not have permission to set superuser status to True."]},
) )
def test_superuser_no_perm_no_superuser(self):
"""Test creating a group without permission and without superuser flag"""
assign_perm("authentik_core.add_group", self.login_user)
self.client.force_login(self.login_user)
res = self.client.post(
reverse("authentik_api:group-list"),
data={"name": generate_id(), "is_superuser": False},
)
self.assertEqual(res.status_code, 201)
def test_superuser_update_no_perm(self): def test_superuser_update_no_perm(self):
"""Test updating a superuser group without permission""" """Test updating a superuser group without permission"""
group = Group.objects.create(name=generate_id(), is_superuser=True) group = Group.objects.create(name=generate_id(), is_superuser=True)

View File

@ -132,14 +132,13 @@ class LicenseKey:
"""Get a summarized version of all (not expired) licenses""" """Get a summarized version of all (not expired) licenses"""
total = LicenseKey(get_license_aud(), 0, "Summarized license", 0, 0) total = LicenseKey(get_license_aud(), 0, "Summarized license", 0, 0)
for lic in License.objects.all(): for lic in License.objects.all():
if lic.is_valid: total.internal_users += lic.internal_users
total.internal_users += lic.internal_users total.external_users += lic.external_users
total.external_users += lic.external_users
total.license_flags.extend(lic.status.license_flags)
exp_ts = int(mktime(lic.expiry.timetuple())) exp_ts = int(mktime(lic.expiry.timetuple()))
if total.exp == 0: if total.exp == 0:
total.exp = exp_ts total.exp = exp_ts
total.exp = max(total.exp, exp_ts) total.exp = max(total.exp, exp_ts)
total.license_flags.extend(lic.status.license_flags)
return total return total
@staticmethod @staticmethod

View File

@ -39,10 +39,6 @@ class License(SerializerModel):
internal_users = models.BigIntegerField() internal_users = models.BigIntegerField()
external_users = models.BigIntegerField() external_users = models.BigIntegerField()
@property
def is_valid(self) -> bool:
return self.expiry >= now()
@property @property
def serializer(self) -> type[BaseSerializer]: def serializer(self) -> type[BaseSerializer]:
from authentik.enterprise.api import LicenseSerializer from authentik.enterprise.api import LicenseSerializer

View File

@ -8,7 +8,6 @@ from django.test import TestCase
from django.utils.timezone import now from django.utils.timezone import now
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from authentik.core.models import User
from authentik.enterprise.license import LicenseKey from authentik.enterprise.license import LicenseKey
from authentik.enterprise.models import ( from authentik.enterprise.models import (
THRESHOLD_READ_ONLY_WEEKS, THRESHOLD_READ_ONLY_WEEKS,
@ -72,9 +71,9 @@ class TestEnterpriseLicense(TestCase):
) )
def test_valid_multiple(self): def test_valid_multiple(self):
"""Check license verification""" """Check license verification"""
lic = License.objects.create(key=generate_id(), expiry=expiry_valid) lic = License.objects.create(key=generate_id())
self.assertTrue(lic.status.status().is_valid) self.assertTrue(lic.status.status().is_valid)
lic2 = License.objects.create(key=generate_id(), expiry=expiry_valid) lic2 = License.objects.create(key=generate_id())
self.assertTrue(lic2.status.status().is_valid) self.assertTrue(lic2.status.status().is_valid)
total = LicenseKey.get_total() total = LicenseKey.get_total()
self.assertEqual(total.internal_users, 200) self.assertEqual(total.internal_users, 200)
@ -233,9 +232,7 @@ class TestEnterpriseLicense(TestCase):
) )
def test_expiry_expired(self): def test_expiry_expired(self):
"""Check license verification""" """Check license verification"""
User.objects.all().delete() License.objects.create(key=generate_id())
License.objects.all().delete()
License.objects.create(key=generate_id(), expiry=expiry_expired)
self.assertEqual(LicenseKey.get_total().summary().status, LicenseUsageStatus.EXPIRED) self.assertEqual(LicenseKey.get_total().summary().status, LicenseUsageStatus.EXPIRED)
@patch( @patch(

View File

@ -15,7 +15,6 @@
{% endblock %} {% endblock %}
<link rel="stylesheet" type="text/css" href="{% static 'dist/sfe/bootstrap.min.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'dist/sfe/bootstrap.min.css' %}">
<meta name="sentry-trace" content="{{ sentry_trace }}" /> <meta name="sentry-trace" content="{{ sentry_trace }}" />
<link rel="prefetch" href="{{ flow_background_url }}" />
{% include "base/header_js.html" %} {% include "base/header_js.html" %}
<style> <style>
html, html,
@ -23,7 +22,7 @@
height: 100%; height: 100%;
} }
body { body {
background-image: url("{{ flow_background_url }}"); background-image: url("{{ flow.background_url }}");
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: cover; background-size: cover;
} }

View File

@ -5,9 +5,9 @@
{% block head_before %} {% block head_before %}
{{ block.super }} {{ block.super }}
<link rel="prefetch" href="{{ flow_background_url }}" /> <link rel="prefetch" href="{{ flow.background_url }}" />
{% if flow.compatibility_mode and not inspector %} {% if flow.compatibility_mode and not inspector %}
<script>ShadyDOM = { force: true };</script> <script>ShadyDOM = { force: !navigator.webdriver };</script>
{% endif %} {% endif %}
{% include "base/header_js.html" %} {% include "base/header_js.html" %}
<script> <script>
@ -21,7 +21,7 @@ window.authentik.flow = {
<script src="{% versioned_script 'dist/flow/FlowInterface-%v.js' %}" type="module"></script> <script src="{% versioned_script 'dist/flow/FlowInterface-%v.js' %}" type="module"></script>
<style> <style>
:root { :root {
--ak-flow-background: url("{{ flow_background_url }}"); --ak-flow-background: url("{{ flow.background_url }}");
} }
</style> </style>
{% endblock %} {% endblock %}

View File

@ -13,9 +13,7 @@ class FlowInterfaceView(InterfaceView):
"""Flow interface""" """Flow interface"""
def get_context_data(self, **kwargs: Any) -> dict[str, Any]: def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
flow = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug")) kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
kwargs["flow"] = flow
kwargs["flow_background_url"] = flow.background_url(self.request)
kwargs["inspector"] = "inspector" in self.request.GET kwargs["inspector"] = "inspector" in self.request.GET
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)

View File

@ -363,9 +363,6 @@ def django_db_config(config: ConfigLoader | None = None) -> dict:
pool_options = config.get_dict_from_b64_json("postgresql.pool_options", True) pool_options = config.get_dict_from_b64_json("postgresql.pool_options", True)
if not pool_options: if not pool_options:
pool_options = True pool_options = True
# FIXME: Temporarily force pool to be deactivated.
# See https://github.com/goauthentik/authentik/issues/14320
pool_options = False
db = { db = {
"default": { "default": {

View File

@ -17,7 +17,7 @@ from ldap3.core.exceptions import LDAPException
from redis.exceptions import ConnectionError as RedisConnectionError from redis.exceptions import ConnectionError as RedisConnectionError
from redis.exceptions import RedisError, ResponseError from redis.exceptions import RedisError, ResponseError
from rest_framework.exceptions import APIException from rest_framework.exceptions import APIException
from sentry_sdk import HttpTransport, get_current_scope from sentry_sdk import HttpTransport
from sentry_sdk import init as sentry_sdk_init from sentry_sdk import init as sentry_sdk_init
from sentry_sdk.api import set_tag from sentry_sdk.api import set_tag
from sentry_sdk.integrations.argv import ArgvIntegration from sentry_sdk.integrations.argv import ArgvIntegration
@ -27,7 +27,6 @@ from sentry_sdk.integrations.redis import RedisIntegration
from sentry_sdk.integrations.socket import SocketIntegration from sentry_sdk.integrations.socket import SocketIntegration
from sentry_sdk.integrations.stdlib import StdlibIntegration from sentry_sdk.integrations.stdlib import StdlibIntegration
from sentry_sdk.integrations.threading import ThreadingIntegration from sentry_sdk.integrations.threading import ThreadingIntegration
from sentry_sdk.tracing import BAGGAGE_HEADER_NAME, SENTRY_TRACE_HEADER_NAME
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from websockets.exceptions import WebSocketException from websockets.exceptions import WebSocketException
@ -96,8 +95,6 @@ def traces_sampler(sampling_context: dict) -> float:
return 0 return 0
if _type == "websocket": if _type == "websocket":
return 0 return 0
if CONFIG.get_bool("debug"):
return 1
return float(CONFIG.get("error_reporting.sample_rate", 0.1)) return float(CONFIG.get("error_reporting.sample_rate", 0.1))
@ -170,14 +167,3 @@ def before_send(event: dict, hint: dict) -> dict | None:
if settings.DEBUG: if settings.DEBUG:
return None return None
return event return event
def get_http_meta():
"""Get sentry-related meta key-values"""
scope = get_current_scope()
meta = {
SENTRY_TRACE_HEADER_NAME: scope.get_traceparent() or "",
}
if bag := scope.get_baggage():
meta[BAGGAGE_HEADER_NAME] = bag.serialize()
return meta

View File

@ -494,88 +494,86 @@ class TestConfig(TestCase):
}, },
) )
# FIXME: Temporarily force pool to be deactivated. def test_db_pool(self):
# See https://github.com/goauthentik/authentik/issues/14320 """Test DB Config with pool"""
# def test_db_pool(self): config = ConfigLoader()
# """Test DB Config with pool""" config.set("postgresql.host", "foo")
# config = ConfigLoader() config.set("postgresql.name", "foo")
# config.set("postgresql.host", "foo") config.set("postgresql.user", "foo")
# config.set("postgresql.name", "foo") config.set("postgresql.password", "foo")
# config.set("postgresql.user", "foo") config.set("postgresql.port", "foo")
# config.set("postgresql.password", "foo") config.set("postgresql.test.name", "foo")
# config.set("postgresql.port", "foo") config.set("postgresql.use_pool", True)
# config.set("postgresql.test.name", "foo") conf = django_db_config(config)
# config.set("postgresql.use_pool", True) self.assertEqual(
# conf = django_db_config(config) conf,
# self.assertEqual( {
# conf, "default": {
# { "ENGINE": "authentik.root.db",
# "default": { "HOST": "foo",
# "ENGINE": "authentik.root.db", "NAME": "foo",
# "HOST": "foo", "OPTIONS": {
# "NAME": "foo", "pool": True,
# "OPTIONS": { "sslcert": None,
# "pool": True, "sslkey": None,
# "sslcert": None, "sslmode": None,
# "sslkey": None, "sslrootcert": None,
# "sslmode": None, },
# "sslrootcert": None, "PASSWORD": "foo",
# }, "PORT": "foo",
# "PASSWORD": "foo", "TEST": {"NAME": "foo"},
# "PORT": "foo", "USER": "foo",
# "TEST": {"NAME": "foo"}, "CONN_MAX_AGE": 0,
# "USER": "foo", "CONN_HEALTH_CHECKS": False,
# "CONN_MAX_AGE": 0, "DISABLE_SERVER_SIDE_CURSORS": False,
# "CONN_HEALTH_CHECKS": False, }
# "DISABLE_SERVER_SIDE_CURSORS": False, },
# } )
# },
# )
# def test_db_pool_options(self): def test_db_pool_options(self):
# """Test DB Config with pool""" """Test DB Config with pool"""
# config = ConfigLoader() config = ConfigLoader()
# config.set("postgresql.host", "foo") config.set("postgresql.host", "foo")
# config.set("postgresql.name", "foo") config.set("postgresql.name", "foo")
# config.set("postgresql.user", "foo") config.set("postgresql.user", "foo")
# config.set("postgresql.password", "foo") config.set("postgresql.password", "foo")
# config.set("postgresql.port", "foo") config.set("postgresql.port", "foo")
# config.set("postgresql.test.name", "foo") config.set("postgresql.test.name", "foo")
# config.set("postgresql.use_pool", True) config.set("postgresql.use_pool", True)
# config.set( config.set(
# "postgresql.pool_options", "postgresql.pool_options",
# base64.b64encode( base64.b64encode(
# dumps( dumps(
# { {
# "max_size": 15, "max_size": 15,
# } }
# ).encode() ).encode()
# ).decode(), ).decode(),
# ) )
# conf = django_db_config(config) conf = django_db_config(config)
# self.assertEqual( self.assertEqual(
# conf, conf,
# { {
# "default": { "default": {
# "ENGINE": "authentik.root.db", "ENGINE": "authentik.root.db",
# "HOST": "foo", "HOST": "foo",
# "NAME": "foo", "NAME": "foo",
# "OPTIONS": { "OPTIONS": {
# "pool": { "pool": {
# "max_size": 15, "max_size": 15,
# }, },
# "sslcert": None, "sslcert": None,
# "sslkey": None, "sslkey": None,
# "sslmode": None, "sslmode": None,
# "sslrootcert": None, "sslrootcert": None,
# }, },
# "PASSWORD": "foo", "PASSWORD": "foo",
# "PORT": "foo", "PORT": "foo",
# "TEST": {"NAME": "foo"}, "TEST": {"NAME": "foo"},
# "USER": "foo", "USER": "foo",
# "CONN_MAX_AGE": 0, "CONN_MAX_AGE": 0,
# "CONN_HEALTH_CHECKS": False, "CONN_HEALTH_CHECKS": False,
# "DISABLE_SERVER_SIDE_CURSORS": False, "DISABLE_SERVER_SIDE_CURSORS": False,
# } }
# }, },
# ) )

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema", "$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://goauthentik.io/blueprints/schema.json", "$id": "https://goauthentik.io/blueprints/schema.json",
"type": "object", "type": "object",
"title": "authentik 2025.4.1 Blueprint schema", "title": "authentik 2025.4.0 Blueprint schema",
"required": [ "required": [
"version", "version",
"entries" "entries"

View File

@ -31,7 +31,7 @@ services:
volumes: volumes:
- redis:/data - redis:/data
server: server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.4.1} image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.4.0}
restart: unless-stopped restart: unless-stopped
command: server command: server
environment: environment:
@ -55,7 +55,7 @@ services:
redis: redis:
condition: service_healthy condition: service_healthy
worker: worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.4.1} image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.4.0}
restart: unless-stopped restart: unless-stopped
command: worker command: worker
environment: environment:

4
go.mod
View File

@ -5,7 +5,7 @@ go 1.24.0
require ( require (
beryju.io/ldap v0.1.0 beryju.io/ldap v0.1.0
github.com/coreos/go-oidc/v3 v3.14.1 github.com/coreos/go-oidc/v3 v3.14.1
github.com/getsentry/sentry-go v0.33.0 github.com/getsentry/sentry-go v0.32.0
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1 github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
github.com/go-ldap/ldap/v3 v3.4.11 github.com/go-ldap/ldap/v3 v3.4.11
github.com/go-openapi/runtime v0.28.0 github.com/go-openapi/runtime v0.28.0
@ -27,7 +27,7 @@ require (
github.com/spf13/cobra v1.9.1 github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
github.com/wwt/guac v1.3.2 github.com/wwt/guac v1.3.2
goauthentik.io/api/v3 v3.2025041.1 goauthentik.io/api/v3 v3.2025040.1
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.30.0 golang.org/x/oauth2 v0.30.0
golang.org/x/sync v0.14.0 golang.org/x/sync v0.14.0

8
go.sum
View File

@ -69,8 +69,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/getsentry/sentry-go v0.33.0 h1:YWyDii0KGVov3xOaamOnF0mjOrqSjBqwv48UEzn7QFg= github.com/getsentry/sentry-go v0.32.0 h1:YKs+//QmwE3DcYtfKRH8/KyOOF/I6Qnx7qYGNHCGmCY=
github.com/getsentry/sentry-go v0.33.0/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE= github.com/getsentry/sentry-go v0.32.0/go.mod h1:CYNcMMz73YigoHljQRG+qPF+eMq8gG72XcGN/p71BAY=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
@ -290,8 +290,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
goauthentik.io/api/v3 v3.2025041.1 h1:GAN6AoTmfnCGgx1SyM07jP4/LR/T3rkTEyShSBd3Co8= goauthentik.io/api/v3 v3.2025040.1 h1:rQEcMNpz84/LPX8LVFteOJuserrd4PnU4k1Iu/wWqhs=
goauthentik.io/api/v3 v3.2025041.1/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= goauthentik.io/api/v3 v3.2025040.1/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=

View File

@ -29,4 +29,4 @@ func UserAgent() string {
return fmt.Sprintf("authentik@%s", FullVersion()) return fmt.Sprintf("authentik@%s", FullVersion())
} }
const VERSION = "2025.4.1" const VERSION = "2025.4.0"

View File

@ -56,7 +56,6 @@ EXPOSE 3389 6636 9300
USER 1000 USER 1000
ENV TMPDIR=/dev/shm/ \ ENV GOFIPS=1
GOFIPS=1
ENTRYPOINT ["/ldap"] ENTRYPOINT ["/ldap"]

View File

@ -97,7 +97,6 @@ elif [[ "$1" == "test-all" ]]; then
elif [[ "$1" == "healthcheck" ]]; then elif [[ "$1" == "healthcheck" ]]; then
run_authentik healthcheck $(cat $MODE_FILE) run_authentik healthcheck $(cat $MODE_FILE)
elif [[ "$1" == "dump_config" ]]; then elif [[ "$1" == "dump_config" ]]; then
shift
exec python -m authentik.lib.config $@ exec python -m authentik.lib.config $@
elif [[ "$1" == "debug" ]]; then elif [[ "$1" == "debug" ]]; then
exec sleep infinity exec sleep infinity

View File

@ -9,7 +9,7 @@
"version": "0.0.0", "version": "0.0.0",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"aws-cdk": "^2.1015.0", "aws-cdk": "^2.1013.0",
"cross-env": "^7.0.3" "cross-env": "^7.0.3"
}, },
"engines": { "engines": {
@ -17,9 +17,9 @@
} }
}, },
"node_modules/aws-cdk": { "node_modules/aws-cdk": {
"version": "2.1015.0", "version": "2.1013.0",
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1015.0.tgz", "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1013.0.tgz",
"integrity": "sha512-txd+yMVVybtLfiwT409+fahbP0SkiwhmQvQf6PVVYnWzDPSknxYlUNJHisHV4tJEcbHWn1QPsLmqqMT0bw8hBg==", "integrity": "sha512-cbq4cOoEIZueMWenGgfI4RujS+AQ9GaMCTlW/3CnvEIhMD8j/tgZx7PTtgMuvwYrRoEeb/wTxgLPgUd5FhsoHA==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {

View File

@ -10,7 +10,7 @@
"node": ">=20" "node": ">=20"
}, },
"devDependencies": { "devDependencies": {
"aws-cdk": "^2.1015.0", "aws-cdk": "^2.1013.0",
"cross-env": "^7.0.3" "cross-env": "^7.0.3"
} }
} }

View File

@ -26,7 +26,7 @@ Parameters:
Description: authentik Docker image Description: authentik Docker image
AuthentikVersion: AuthentikVersion:
Type: String Type: String
Default: 2025.4.1 Default: 2025.4.0
Description: authentik Docker image tag Description: authentik Docker image tag
AuthentikServerCPU: AuthentikServerCPU:
Type: Number Type: Number

View File

@ -1,6 +1,6 @@
{ {
"name": "@goauthentik/authentik", "name": "@goauthentik/authentik",
"version": "2025.4.1", "version": "2025.4.0",
"private": true, "private": true,
"type": "module", "type": "module",
"devDependencies": { "devDependencies": {

View File

@ -76,7 +76,6 @@ EXPOSE 9000 9300 9443
USER 1000 USER 1000
ENV TMPDIR=/dev/shm/ \ ENV GOFIPS=1
GOFIPS=1
ENTRYPOINT ["/proxy"] ENTRYPOINT ["/proxy"]

View File

@ -1,104 +1,104 @@
[project] [project]
name = "authentik" name = "authentik"
version = "2025.4.1" version = "2025.4.0"
description = "" description = ""
authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }] authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }]
requires-python = "==3.13.*" requires-python = "==3.13.*"
dependencies = [ dependencies = [
"argon2-cffi==23.1.0", "argon2-cffi",
"celery==5.5.2", "celery",
"channels==4.2.2", "channels",
"channels-redis==4.2.1", "channels-redis",
"cryptography==44.0.3", "cryptography",
"dacite==1.9.2", "dacite",
"deepmerge==2.0", "deepmerge",
"defusedxml==0.7.1", "defusedxml",
"django==5.1.9", "django",
"django-countries==7.6.1", "django-countries",
"django-cte==1.3.3", "django-cte",
"django-filter==25.1", "django-filter",
"django-guardian<3.0.0", "django-guardian",
"django-model-utils==5.0.0", "django-model-utils",
"django-pglock==1.7.2", "django-pglock",
"django-prometheus==2.3.1", "django-prometheus",
"django-redis==5.4.0", "django-redis",
"django-storages[s3]==1.14.6", "django-storages[s3]",
"django-tenants==3.7.0", "django-tenants",
"djangorestframework==3.16.0", "djangorestframework",
"djangorestframework-guardian==0.3.0", "djangorestframework-guardian",
"docker==7.1.0", "docker",
"drf-orjson-renderer==1.7.3", "drf-orjson-renderer",
"drf-spectacular==0.28.0", "drf-spectacular",
"dumb-init==1.2.5.post1", "dumb-init",
"duo-client==5.5.0", "duo-client",
"fido2==1.2.0", "fido2",
"flower==2.0.1", "flower",
"geoip2==5.1.0", "geoip2",
"geopy==2.4.1", "geopy",
"google-api-python-client==2.169.0", "google-api-python-client",
"gssapi==1.9.0", "gssapi",
"gunicorn==23.0.0", "gunicorn",
"jsonpatch==1.33", "jsonpatch",
"jwcrypto==1.5.6", "jwcrypto",
"kubernetes==32.0.1", "kubernetes",
"ldap3==2.9.1", "ldap3",
"lxml==5.4.0", "lxml",
"msgraph-sdk==1.30.0", "msgraph-sdk",
"opencontainers==0.0.14", "opencontainers",
"packaging==25.0", "packaging",
"paramiko==3.5.1", "paramiko",
"psycopg[c,pool]==3.2.9", "psycopg[c, pool]",
"pydantic==2.11.4", "pydantic",
"pydantic-scim==0.0.8", "pydantic-scim",
"pyjwt==2.10.1", "pyjwt",
"pyrad==2.4", "pyrad",
"python-kadmin-rs==0.6.0", "python-kadmin-rs",
"pyyaml==6.0.2", "pyyaml",
"requests-oauthlib==2.0.0", "requests-oauthlib",
"scim2-filter-parser==0.7.0", "scim2-filter-parser",
"sentry-sdk==2.28.0", "sentry-sdk",
"service-identity==24.2.0", "service_identity",
"setproctitle==1.3.6", "setproctitle",
"structlog==25.3.0", "structlog",
"swagger-spec-validator==3.0.4", "swagger-spec-validator",
"tenant-schemas-celery==4.0.1", "tenant-schemas-celery",
"twilio==9.6.1", "twilio",
"ua-parser==1.0.1", "ua-parser",
"unidecode==1.4.0", "unidecode",
"urllib3<3", "urllib3 <3",
"uvicorn[standard]==0.34.2", "uvicorn[standard]",
"watchdog==6.0.0", "watchdog",
"webauthn==2.5.2", "webauthn",
"wsproto==1.2.0", "wsproto",
"xmlsec==1.3.15", "xmlsec",
"zxcvbn==4.5.0", "zxcvbn",
] ]
[dependency-groups] [dependency-groups]
dev = [ dev = [
"aws-cdk-lib==2.188.0", "aws-cdk-lib",
"bandit==1.8.3", "bandit",
"black==25.1.0", "black",
"bump2version==1.0.1", "bump2version",
"channels[daphne]==4.2.2", "channels[daphne]",
"codespell==2.4.1", "codespell",
"colorama==0.4.6", "colorama",
"constructs==10.4.2", "constructs",
"coverage[toml]==7.8.0", "coverage[toml]",
"debugpy==1.8.14", "debugpy",
"drf-jsonschema-serializer==3.0.0", "drf-jsonschema-serializer",
"freezegun==1.5.1", "freezegun",
"importlib-metadata==8.6.1", "importlib-metadata",
"k5test==0.10.4", "k5test",
"pdoc==15.0.3", "pdoc",
"pytest==8.3.5", "pytest",
"pytest-django==4.11.1", "pytest-django",
"pytest-github-actions-annotate-failures==0.3.0", "pytest-github-actions-annotate-failures",
"pytest-randomly==3.16.0", "pytest-randomly",
"pytest-timeout==2.4.0", "pytest-timeout",
"requests-mock==1.12.1", "requests-mock",
"ruff==0.11.9", "ruff",
"selenium==4.32.0", "selenium",
] ]
[tool.uv] [tool.uv]

View File

@ -56,7 +56,6 @@ HEALTHCHECK --interval=5s --retries=20 --start-period=3s CMD [ "/rac", "healthch
USER 1000 USER 1000
ENV TMPDIR=/dev/shm/ \ ENV GOFIPS=1
GOFIPS=1
ENTRYPOINT ["/rac"] ENTRYPOINT ["/rac"]

View File

@ -56,7 +56,6 @@ EXPOSE 1812/udp 9300
USER 1000 USER 1000
ENV TMPDIR=/dev/shm/ \ ENV GOFIPS=1
GOFIPS=1
ENTRYPOINT ["/radius"] ENTRYPOINT ["/radius"]

View File

@ -1,7 +1,7 @@
openapi: 3.0.3 openapi: 3.0.3
info: info:
title: authentik title: authentik
version: 2025.4.1 version: 2025.4.0
description: Making authentication simple. description: Making authentication simple.
contact: contact:
email: hello@goauthentik.io email: hello@goauthentik.io

View File

@ -1,19 +1,12 @@
"""test default login flow""" """test default login flow"""
from authentik.blueprints.tests import apply_blueprint from authentik.blueprints.tests import apply_blueprint
from authentik.flows.models import Flow
from tests.e2e.utils import SeleniumTestCase, retry from tests.e2e.utils import SeleniumTestCase, retry
class TestFlowsLogin(SeleniumTestCase): class TestFlowsLogin(SeleniumTestCase):
"""test default login flow""" """test default login flow"""
def tearDown(self):
# Reset authentication flow's compatibility mode; we need to do this as its
# not specified in the blueprint
Flow.objects.filter(slug="default-authentication-flow").update(compatibility_mode=False)
return super().tearDown()
@retry() @retry()
@apply_blueprint( @apply_blueprint(
"default/flow-default-authentication-flow.yaml", "default/flow-default-authentication-flow.yaml",
@ -30,21 +23,3 @@ class TestFlowsLogin(SeleniumTestCase):
self.login() self.login()
self.wait_for_url(self.if_user_url("/library")) self.wait_for_url(self.if_user_url("/library"))
self.assert_user(self.user) self.assert_user(self.user)
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
"default/flow-default-invalidation-flow.yaml",
)
def test_login_compatibility_mode(self):
"""test default login flow with compatibility mode enabled"""
Flow.objects.filter(slug="default-authentication-flow").update(compatibility_mode=True)
self.driver.get(
self.url(
"authentik_core:if-flow",
flow_slug="default-authentication-flow",
)
)
self.login(shadow_dom=False)
self.wait_for_url(self.if_user_url("/library"))
self.assert_user(self.user)

View File

@ -1,51 +0,0 @@
"""test default login (using SFE interface) flow"""
from time import sleep
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from authentik.blueprints.tests import apply_blueprint
from tests.e2e.utils import SeleniumTestCase, retry
class TestFlowsLoginSFE(SeleniumTestCase):
"""test default login flow"""
def login(self):
"""Do entire login flow adjusted for SFE"""
flow_executor = self.driver.find_element(By.ID, "flow-sfe-container")
identification_stage = flow_executor.find_element(By.ID, "ident-form")
identification_stage.find_element(By.CSS_SELECTOR, "input[name=uid_field]").click()
identification_stage.find_element(By.CSS_SELECTOR, "input[name=uid_field]").send_keys(
self.user.username
)
identification_stage.find_element(By.CSS_SELECTOR, "input[name=uid_field]").send_keys(
Keys.ENTER
)
password_stage = flow_executor.find_element(By.ID, "password-form")
password_stage.find_element(By.CSS_SELECTOR, "input[name=password]").send_keys(
self.user.username
)
password_stage.find_element(By.CSS_SELECTOR, "input[name=password]").send_keys(Keys.ENTER)
sleep(1)
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
"default/flow-default-invalidation-flow.yaml",
)
def test_login(self):
"""test default login flow"""
self.driver.get(
self.url(
"authentik_core:if-flow",
flow_slug="default-authentication-flow",
query={"sfe": True},
)
)
self.login()
self.wait_for_url(self.if_user_url("/library"))
self.assert_user(self.user)

View File

@ -29,7 +29,6 @@ from selenium.webdriver.common.keys import Keys
from selenium.webdriver.remote.command import Command from selenium.webdriver.remote.command import Command
from selenium.webdriver.remote.webdriver import WebDriver from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.remote.webelement import WebElement from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support import expected_conditions as ec
from selenium.webdriver.support.wait import WebDriverWait from selenium.webdriver.support.wait import WebDriverWait
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
@ -38,8 +37,8 @@ from authentik.core.models import User
from authentik.core.tests.utils import create_test_admin_user from authentik.core.tests.utils import create_test_admin_user
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id
RETRIES = int(environ.get("RETRIES", "3"))
IS_CI = "CI" in environ IS_CI = "CI" in environ
RETRIES = int(environ.get("RETRIES", "3")) if IS_CI else 1
def get_docker_tag() -> str: def get_docker_tag() -> str:
@ -241,30 +240,10 @@ class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase):
element = self.driver.execute_script("return arguments[0].shadowRoot", shadow_root) element = self.driver.execute_script("return arguments[0].shadowRoot", shadow_root)
return element return element
def shady_dom(self) -> WebElement: def login(self):
class wrapper: """Do entire login flow and check user afterwards"""
def __init__(self, container: WebDriver): flow_executor = self.get_shadow_root("ak-flow-executor")
self.container = container identification_stage = self.get_shadow_root("ak-stage-identification", flow_executor)
def find_element(self, by: str, selector: str) -> WebElement:
return self.container.execute_script(
"return document.__shady_native_querySelector(arguments[0])", selector
)
return wrapper(self.driver)
def login(self, shadow_dom=True):
"""Do entire login flow"""
if shadow_dom:
flow_executor = self.get_shadow_root("ak-flow-executor")
identification_stage = self.get_shadow_root("ak-stage-identification", flow_executor)
else:
flow_executor = self.shady_dom()
identification_stage = self.shady_dom()
wait = WebDriverWait(identification_stage, self.wait_timeout)
wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "input[name=uidField]")))
identification_stage.find_element(By.CSS_SELECTOR, "input[name=uidField]").click() identification_stage.find_element(By.CSS_SELECTOR, "input[name=uidField]").click()
identification_stage.find_element(By.CSS_SELECTOR, "input[name=uidField]").send_keys( identification_stage.find_element(By.CSS_SELECTOR, "input[name=uidField]").send_keys(
@ -274,16 +253,8 @@ class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase):
Keys.ENTER Keys.ENTER
) )
if shadow_dom: flow_executor = self.get_shadow_root("ak-flow-executor")
flow_executor = self.get_shadow_root("ak-flow-executor") password_stage = self.get_shadow_root("ak-stage-password", flow_executor)
password_stage = self.get_shadow_root("ak-stage-password", flow_executor)
else:
flow_executor = self.shady_dom()
password_stage = self.shady_dom()
wait = WebDriverWait(password_stage, self.wait_timeout)
wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "input[name=password]")))
password_stage.find_element(By.CSS_SELECTOR, "input[name=password]").send_keys( password_stage.find_element(By.CSS_SELECTOR, "input[name=password]").send_keys(
self.user.username self.user.username
) )

321
uv.lock generated
View File

@ -164,7 +164,7 @@ wheels = [
[[package]] [[package]]
name = "authentik" name = "authentik"
version = "2025.4.1" version = "2025.4.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "argon2-cffi" }, { name = "argon2-cffi" },
@ -265,100 +265,100 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "argon2-cffi", specifier = "==23.1.0" }, { name = "argon2-cffi" },
{ name = "celery", specifier = "==5.5.2" }, { name = "celery" },
{ name = "channels", specifier = "==4.2.2" }, { name = "channels" },
{ name = "channels-redis", specifier = "==4.2.1" }, { name = "channels-redis" },
{ name = "cryptography", specifier = "==44.0.3" }, { name = "cryptography" },
{ name = "dacite", specifier = "==1.9.2" }, { name = "dacite" },
{ name = "deepmerge", specifier = "==2.0" }, { name = "deepmerge" },
{ name = "defusedxml", specifier = "==0.7.1" }, { name = "defusedxml" },
{ name = "django", specifier = "==5.1.9" }, { name = "django" },
{ name = "django-countries", specifier = "==7.6.1" }, { name = "django-countries" },
{ name = "django-cte", specifier = "==1.3.3" }, { name = "django-cte" },
{ name = "django-filter", specifier = "==25.1" }, { name = "django-filter" },
{ name = "django-guardian", specifier = "<3.0.0" }, { name = "django-guardian" },
{ name = "django-model-utils", specifier = "==5.0.0" }, { name = "django-model-utils" },
{ name = "django-pglock", specifier = "==1.7.2" }, { name = "django-pglock" },
{ name = "django-prometheus", specifier = "==2.3.1" }, { name = "django-prometheus" },
{ name = "django-redis", specifier = "==5.4.0" }, { name = "django-redis" },
{ name = "django-storages", extras = ["s3"], specifier = "==1.14.6" }, { name = "django-storages", extras = ["s3"] },
{ name = "django-tenants", git = "https://github.com/rissson/django-tenants.git?branch=authentik-fixes" }, { name = "django-tenants", git = "https://github.com/rissson/django-tenants.git?branch=authentik-fixes" },
{ name = "djangorestframework", git = "https://github.com/authentik-community/django-rest-framework?rev=896722bab969fabc74a08b827da59409cf9f1a4e" }, { name = "djangorestframework", git = "https://github.com/authentik-community/django-rest-framework?rev=896722bab969fabc74a08b827da59409cf9f1a4e" },
{ name = "djangorestframework-guardian", specifier = "==0.3.0" }, { name = "djangorestframework-guardian" },
{ name = "docker", specifier = "==7.1.0" }, { name = "docker" },
{ name = "drf-orjson-renderer", specifier = "==1.7.3" }, { name = "drf-orjson-renderer" },
{ name = "drf-spectacular", specifier = "==0.28.0" }, { name = "drf-spectacular" },
{ name = "dumb-init", specifier = "==1.2.5.post1" }, { name = "dumb-init" },
{ name = "duo-client", specifier = "==5.5.0" }, { name = "duo-client" },
{ name = "fido2", specifier = "==1.2.0" }, { name = "fido2" },
{ name = "flower", specifier = "==2.0.1" }, { name = "flower" },
{ name = "geoip2", specifier = "==5.1.0" }, { name = "geoip2" },
{ name = "geopy", specifier = "==2.4.1" }, { name = "geopy" },
{ name = "google-api-python-client", specifier = "==2.169.0" }, { name = "google-api-python-client" },
{ name = "gssapi", specifier = "==1.9.0" }, { name = "gssapi" },
{ name = "gunicorn", specifier = "==23.0.0" }, { name = "gunicorn" },
{ name = "jsonpatch", specifier = "==1.33" }, { name = "jsonpatch" },
{ name = "jwcrypto", specifier = "==1.5.6" }, { name = "jwcrypto" },
{ name = "kubernetes", specifier = "==32.0.1" }, { name = "kubernetes" },
{ name = "ldap3", specifier = "==2.9.1" }, { name = "ldap3" },
{ name = "lxml", specifier = "==5.4.0" }, { name = "lxml" },
{ name = "msgraph-sdk", specifier = "==1.30.0" }, { name = "msgraph-sdk" },
{ name = "opencontainers", git = "https://github.com/BeryJu/oci-python?rev=c791b19056769cd67957322806809ab70f5bead8" }, { name = "opencontainers", git = "https://github.com/BeryJu/oci-python?rev=c791b19056769cd67957322806809ab70f5bead8" },
{ name = "packaging", specifier = "==25.0" }, { name = "packaging" },
{ name = "paramiko", specifier = "==3.5.1" }, { name = "paramiko" },
{ name = "psycopg", extras = ["c", "pool"], specifier = "==3.2.9" }, { name = "psycopg", extras = ["c", "pool"] },
{ name = "pydantic", specifier = "==2.11.4" }, { name = "pydantic" },
{ name = "pydantic-scim", specifier = "==0.0.8" }, { name = "pydantic-scim" },
{ name = "pyjwt", specifier = "==2.10.1" }, { name = "pyjwt" },
{ name = "pyrad", specifier = "==2.4" }, { name = "pyrad" },
{ name = "python-kadmin-rs", specifier = "==0.6.0" }, { name = "python-kadmin-rs" },
{ name = "pyyaml", specifier = "==6.0.2" }, { name = "pyyaml" },
{ name = "requests-oauthlib", specifier = "==2.0.0" }, { name = "requests-oauthlib" },
{ name = "scim2-filter-parser", specifier = "==0.7.0" }, { name = "scim2-filter-parser" },
{ name = "sentry-sdk", specifier = "==2.28.0" }, { name = "sentry-sdk" },
{ name = "service-identity", specifier = "==24.2.0" }, { name = "service-identity" },
{ name = "setproctitle", specifier = "==1.3.6" }, { name = "setproctitle" },
{ name = "structlog", specifier = "==25.3.0" }, { name = "structlog" },
{ name = "swagger-spec-validator", specifier = "==3.0.4" }, { name = "swagger-spec-validator" },
{ name = "tenant-schemas-celery", specifier = "==4.0.1" }, { name = "tenant-schemas-celery" },
{ name = "twilio", specifier = "==9.6.1" }, { name = "twilio" },
{ name = "ua-parser", specifier = "==1.0.1" }, { name = "ua-parser" },
{ name = "unidecode", specifier = "==1.4.0" }, { name = "unidecode" },
{ name = "urllib3", specifier = "<3" }, { name = "urllib3", specifier = "<3" },
{ name = "uvicorn", extras = ["standard"], specifier = "==0.34.2" }, { name = "uvicorn", extras = ["standard"] },
{ name = "watchdog", specifier = "==6.0.0" }, { name = "watchdog" },
{ name = "webauthn", specifier = "==2.5.2" }, { name = "webauthn" },
{ name = "wsproto", specifier = "==1.2.0" }, { name = "wsproto" },
{ name = "xmlsec", specifier = "==1.3.15" }, { name = "xmlsec" },
{ name = "zxcvbn", specifier = "==4.5.0" }, { name = "zxcvbn" },
] ]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
{ name = "aws-cdk-lib", specifier = "==2.188.0" }, { name = "aws-cdk-lib" },
{ name = "bandit", specifier = "==1.8.3" }, { name = "bandit" },
{ name = "black", specifier = "==25.1.0" }, { name = "black" },
{ name = "bump2version", specifier = "==1.0.1" }, { name = "bump2version" },
{ name = "channels", extras = ["daphne"], specifier = "==4.2.2" }, { name = "channels", extras = ["daphne"] },
{ name = "codespell", specifier = "==2.4.1" }, { name = "codespell" },
{ name = "colorama", specifier = "==0.4.6" }, { name = "colorama" },
{ name = "constructs", specifier = "==10.4.2" }, { name = "constructs" },
{ name = "coverage", extras = ["toml"], specifier = "==7.8.0" }, { name = "coverage", extras = ["toml"] },
{ name = "debugpy", specifier = "==1.8.14" }, { name = "debugpy" },
{ name = "drf-jsonschema-serializer", specifier = "==3.0.0" }, { name = "drf-jsonschema-serializer" },
{ name = "freezegun", specifier = "==1.5.1" }, { name = "freezegun" },
{ name = "importlib-metadata", specifier = "==8.6.1" }, { name = "importlib-metadata" },
{ name = "k5test", specifier = "==0.10.4" }, { name = "k5test" },
{ name = "pdoc", specifier = "==15.0.3" }, { name = "pdoc" },
{ name = "pytest", specifier = "==8.3.5" }, { name = "pytest" },
{ name = "pytest-django", specifier = "==4.11.1" }, { name = "pytest-django" },
{ name = "pytest-github-actions-annotate-failures", specifier = "==0.3.0" }, { name = "pytest-github-actions-annotate-failures" },
{ name = "pytest-randomly", specifier = "==3.16.0" }, { name = "pytest-randomly" },
{ name = "pytest-timeout", specifier = "==2.4.0" }, { name = "pytest-timeout" },
{ name = "requests-mock", specifier = "==1.12.1" }, { name = "requests-mock" },
{ name = "ruff", specifier = "==0.11.9" }, { name = "ruff" },
{ name = "selenium", specifier = "==4.32.0" }, { name = "selenium" },
] ]
[[package]] [[package]]
@ -387,16 +387,16 @@ wheels = [
[[package]] [[package]]
name = "aws-cdk-asset-awscli-v1" name = "aws-cdk-asset-awscli-v1"
version = "2.2.235" version = "2.2.231"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "jsii" }, { name = "jsii" },
{ name = "publication" }, { name = "publication" },
{ name = "typeguard" }, { name = "typeguard" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/20/e8/6706ee98e9ba436aa07ca3a65d79cf40c50005f4f760f139bec0f6c3606a/aws_cdk_asset_awscli_v1-2.2.235.tar.gz", hash = "sha256:0a2023f9d32158ae86d43dfeac2ba7679e8a050cb99b7565b26192e60e57a91c", size = 19130124, upload-time = "2025-05-05T15:24:02.938Z" } sdist = { url = "https://files.pythonhosted.org/packages/01/b2/4a142d1d8093691c1b54b7b35f463f6defa1d0a8a08b7be2277eae73c726/aws_cdk_asset_awscli_v1-2.2.231.tar.gz", hash = "sha256:859d99e0fcdc2f6ada44090ad9f921743da3ca3a6d9f39ab06836d4c8e0fbc23", size = 17960944, upload-time = "2025-04-07T16:48:17.423Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/97/27/b167173d7fb784848563d596085dc8e95cabbe7b01f8a5c0ac1ed6a80c36/aws_cdk_asset_awscli_v1-2.2.235-py3-none-any.whl", hash = "sha256:701a47a97419b917ce73cf9c922a26c2895943b4b18b191e1285572b8584ae1e", size = 19128489, upload-time = "2025-05-05T15:23:59.87Z" }, { url = "https://files.pythonhosted.org/packages/68/2d/dae06874ab3a66ad898d9c2d792c863b8b8249b203a1d8e3b36dfca44a93/aws_cdk_asset_awscli_v1-2.2.231-py3-none-any.whl", hash = "sha256:06d6b1d9e52272c315b944320f7039b47c6a6058f063fa33ab0ec06fea17bfbe", size = 17959325, upload-time = "2025-04-07T16:48:14.477Z" },
] ]
[[package]] [[package]]
@ -571,30 +571,30 @@ wheels = [
[[package]] [[package]]
name = "boto3" name = "boto3"
version = "1.38.13" version = "1.38.10"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "botocore" }, { name = "botocore" },
{ name = "jmespath" }, { name = "jmespath" },
{ name = "s3transfer" }, { name = "s3transfer" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/c7/89/a47f62b3f81a2e3484d2a2b8dd4906c5b6e57da0af0bd59d36f99ba20baf/boto3-1.38.13.tar.gz", hash = "sha256:6633bce2b73284acce1453ca85834c7c5a59e0dbcce1170be461cc079bdcdfcf", size = 111812, upload-time = "2025-05-09T19:33:02.962Z" } sdist = { url = "https://files.pythonhosted.org/packages/5f/26/c4a2f1c64efb5ae6b47b94cb543282ab5770aa2c4562aba6934af628cf76/boto3-1.38.10.tar.gz", hash = "sha256:af4c78a3faa1a56cbaeb9e06cd5580772138be519fc6e740b81db586d5d1910c", size = 111837, upload-time = "2025-05-06T19:29:55.088Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/72/25/79e219648f10d060d152542fcf3be0093120471774b99c1a7f41ceaeca9b/boto3-1.38.13-py3-none-any.whl", hash = "sha256:668400d13889d2d2fcd66ce785cc0b0fc040681f58a9c7f67daa9149a52b6c63", size = 139934, upload-time = "2025-05-09T19:33:00.855Z" }, { url = "https://files.pythonhosted.org/packages/2f/fe/2b69dcdd433c32ba80b36eabfe799e8c3e0b08ff3e0fc06bc2e1cc065a19/boto3-1.38.10-py3-none-any.whl", hash = "sha256:26113a47d549bc3c46dbf56c8ab74f272c3da55df23e2c460fcf3c6c64d54dce", size = 139911, upload-time = "2025-05-06T19:29:51.823Z" },
] ]
[[package]] [[package]]
name = "botocore" name = "botocore"
version = "1.38.13" version = "1.38.10"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "jmespath" }, { name = "jmespath" },
{ name = "python-dateutil" }, { name = "python-dateutil" },
{ name = "urllib3" }, { name = "urllib3" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/de/36/5b0faba074684744244e1e030e73fd5612bc2c38f557eec0a7f1a3d7ddd2/botocore-1.38.13.tar.gz", hash = "sha256:22feee15753cd3f9f7179d041604078a1024701497d27b22be7c6707e8d13ccb", size = 13882010, upload-time = "2025-05-09T19:32:51.172Z" } sdist = { url = "https://files.pythonhosted.org/packages/c0/18/c03b763c831e269d76a7c0fcba53802f99bf68f8d4530af672ae96a6d343/botocore-1.38.10.tar.gz", hash = "sha256:c531c13803e0fad5b395c5ccab4c11ac88acfccde71c9b998df6fa841392a8fc", size = 13881598, upload-time = "2025-05-06T19:29:41.315Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/94/df/a7a8097471d5a3bc7d408850222292d874ffc190aef7e1cacf9af770339e/botocore-1.38.13-py3-none-any.whl", hash = "sha256:de29fee43a1f02787fb5b3756ec09917d5661ed95b2b2d64797ab04196f69e14", size = 13544507, upload-time = "2025-05-09T19:32:37.727Z" }, { url = "https://files.pythonhosted.org/packages/f9/92/2c522e277c95d35b4b83bff6a3839875d91b0d835a93545828a7046013c4/botocore-1.38.10-py3-none-any.whl", hash = "sha256:5244454bb9e8fbb6510145d1554e82fd243e8583507d83077ecf4f8efb66cb46", size = 13539530, upload-time = "2025-05-06T19:29:35.384Z" },
] ]
[[package]] [[package]]
@ -750,14 +750,14 @@ wheels = [
[[package]] [[package]]
name = "click" name = "click"
version = "8.2.0" version = "8.1.8"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" }, { name = "colorama", marker = "sys_platform == 'win32'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/cd/0f/62ca20172d4f87d93cf89665fbaedcd560ac48b465bd1d92bfc7ea6b0a41/click-8.2.0.tar.gz", hash = "sha256:f5452aeddd9988eefa20f90f05ab66f17fce1ee2a36907fd30b05bbb5953814d", size = 235857, upload-time = "2025-05-10T22:21:03.111Z" } sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/a2/58/1f37bf81e3c689cc74ffa42102fa8915b59085f54a6e4a80bc6265c0f6bf/click-8.2.0-py3-none-any.whl", hash = "sha256:6b303f0b2aa85f1cb4e5303078fadcbcd4e476f114fab9b5007005711839325c", size = 102156, upload-time = "2025-05-10T22:21:01.352Z" }, { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" },
] ]
[[package]] [[package]]
@ -979,16 +979,16 @@ wheels = [
[[package]] [[package]]
name = "django" name = "django"
version = "5.1.9" version = "5.1.8"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "asgiref" }, { name = "asgiref" },
{ name = "sqlparse" }, { name = "sqlparse" },
{ name = "tzdata", marker = "sys_platform == 'win32'" }, { name = "tzdata", marker = "sys_platform == 'win32'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/10/08/2e6f05494b3fc0a3c53736846034f882b82ee6351791a7815bbb45715d79/django-5.1.9.tar.gz", hash = "sha256:565881bdd0eb67da36442e9ac788bda90275386b549070d70aee86327781a4fc", size = 10710887, upload-time = "2025-05-07T14:06:45.257Z" } sdist = { url = "https://files.pythonhosted.org/packages/00/40/45adc1b93435d1b418654a734b68351bb6ce0a0e5e37b2f0e9aeb1a2e233/Django-5.1.8.tar.gz", hash = "sha256:42e92a1dd2810072bcc40a39a212b693f94406d0ba0749e68eb642f31dc770b4", size = 10723602, upload-time = "2025-04-02T11:19:56.028Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/d1/d8b6b8250b84380d5a123e099ad3298a49407d81598faa13b43a2c6d96d7/django-5.1.9-py3-none-any.whl", hash = "sha256:2fd1d4a0a66a5ba702699eb692e75b0d828b73cc2f4e1fc4b6a854a918967411", size = 8277363, upload-time = "2025-05-07T14:06:37.426Z" }, { url = "https://files.pythonhosted.org/packages/ec/0d/e6dd0ed898b920fec35c6eeeb9acbeb831fff19ad21c5e684744df1d4a36/Django-5.1.8-py3-none-any.whl", hash = "sha256:11b28fa4b00e59d0def004e9ee012fefbb1065a5beb39ee838983fd24493ad4f", size = 8277130, upload-time = "2025-04-02T11:19:51.591Z" },
] ]
[[package]] [[package]]
@ -1063,15 +1063,15 @@ wheels = [
[[package]] [[package]]
name = "django-pglock" name = "django-pglock"
version = "1.7.2" version = "1.7.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "django" }, { name = "django" },
{ name = "django-pgactivity" }, { name = "django-pgactivity" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/9e/1a/6c552b75d0a6b380215c919a667072e4949a3123f275506c6e6ff82f6b76/django_pglock-1.7.2.tar.gz", hash = "sha256:d1d8521b382a5819e8d14978d0e8e63ab2763cb784f5a6bfdbe5de807da4a61a", size = 17287, upload-time = "2025-05-15T22:07:23.744Z" } sdist = { url = "https://files.pythonhosted.org/packages/ca/9f/3b4b2f7021b626b3981646254f04fcc2db681d7ba7b24be563552368be70/django_pglock-1.7.1.tar.gz", hash = "sha256:69050bdb522fd34585d49bb8a4798dbfbab9ec4754dd1927b1b9eef2ec0edadf", size = 16907, upload-time = "2024-12-16T01:53:47.29Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/d2/21f19531945f03021460d40654bc2fc3b0c474b57b279d5f5a1c34be7f1b/django_pglock-1.7.2-py3-none-any.whl", hash = "sha256:2f9335527779445fe86507b37e26cfde485a32b91d982a8f80039d3bcd25d596", size = 17674, upload-time = "2025-05-15T22:07:22.618Z" }, { url = "https://files.pythonhosted.org/packages/15/bf/6fc72033801279b4ae2003c5b93cc22dfe0814ca1f56432f7cd06975381d/django_pglock-1.7.1-py3-none-any.whl", hash = "sha256:15db418fb56bee37fc8707038495b5085af9b8c203ebfa300202572127bdb3f0", size = 17343, upload-time = "2024-12-16T01:53:45.243Z" },
] ]
[[package]] [[package]]
@ -2065,7 +2065,7 @@ wheels = [
[[package]] [[package]]
name = "msgraph-sdk" name = "msgraph-sdk"
version = "1.30.0" version = "1.28.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "azure-identity" }, { name = "azure-identity" },
@ -2075,9 +2075,9 @@ dependencies = [
{ name = "microsoft-kiota-serialization-text" }, { name = "microsoft-kiota-serialization-text" },
{ name = "msgraph-core" }, { name = "msgraph-core" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/e9/4a/4ff19671f6ea06f98fb2405f73a90350e4719ccc692e85e9e0c2fa066826/msgraph_sdk-1.30.0.tar.gz", hash = "sha256:59e30af6d7244c9009146d620c331e169701b651317746b16f561e2e2452e73f", size = 6608744, upload-time = "2025-05-13T13:09:12.594Z" } sdist = { url = "https://files.pythonhosted.org/packages/b9/41/40bb3c630ca026182aefd79a9862ef4a1917b1161c83690c858d714788f5/msgraph_sdk-1.28.0.tar.gz", hash = "sha256:b2d64b7bd711ad285fc2c090dd524853a026848732e1c83874fe34561805350d", size = 6121069, upload-time = "2025-04-15T11:39:08.184Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/70/95/451ec4db8a924274a1f7260809ea03fe9c2b446d84dc5238e92e49a1b522/msgraph_sdk-1.30.0-py3-none-any.whl", hash = "sha256:6748f5cdb5ddbcff9e4f3fb073dd0a604cb00e1cf285dd0fea6969c93ba8282f", size = 27140767, upload-time = "2025-05-13T13:09:07.718Z" }, { url = "https://files.pythonhosted.org/packages/a8/58/d8e9593ea81779d503831b5b06c8d9881d5affefe3df99ca20112c969e6f/msgraph_sdk-1.28.0-py3-none-any.whl", hash = "sha256:bd33b186371dfa8ed6375dfda92eef0931485633e69b06c001ce3c2fd3658f18", size = 25091309, upload-time = "2025-04-15T11:39:04.968Z" },
] ]
[[package]] [[package]]
@ -2157,42 +2157,42 @@ source = { git = "https://github.com/BeryJu/oci-python?rev=c791b19056769cd679573
[[package]] [[package]]
name = "opentelemetry-api" name = "opentelemetry-api"
version = "1.33.0" version = "1.32.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "deprecated" }, { name = "deprecated" },
{ name = "importlib-metadata" }, { name = "importlib-metadata" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/70/ca/920a73b4a11cd271ba1c62f34dba27d7783996a6a7ac0bac7c83b230736d/opentelemetry_api-1.33.0.tar.gz", hash = "sha256:cc4380fd2e6da7dcb52a828ea81844ed1f4f2eb638ca3c816775109d93d58ced", size = 65000, upload-time = "2025-05-09T14:56:00.967Z" } sdist = { url = "https://files.pythonhosted.org/packages/42/40/2359245cd33641c2736a0136a50813352d72f3fc209de28fb226950db4a1/opentelemetry_api-1.32.1.tar.gz", hash = "sha256:a5be71591694a4d9195caf6776b055aa702e964d961051a0715d05f8632c32fb", size = 64138, upload-time = "2025-04-15T16:02:13.97Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/c4/26c7ec8e51c19632f42503dbabed286c261fb06f8f61ffd348690e36958a/opentelemetry_api-1.33.0-py3-none-any.whl", hash = "sha256:158df154f628e6615b65fdf6e59f99afabea7213e72c5809dd4adf06c0d997cd", size = 65772, upload-time = "2025-05-09T14:55:38.395Z" }, { url = "https://files.pythonhosted.org/packages/12/f2/89ea3361a305466bc6460a532188830351220b5f0851a5fa133155c16eca/opentelemetry_api-1.32.1-py3-none-any.whl", hash = "sha256:bbd19f14ab9f15f0e85e43e6a958aa4cb1f36870ee62b7fd205783a112012724", size = 65287, upload-time = "2025-04-15T16:01:49.747Z" },
] ]
[[package]] [[package]]
name = "opentelemetry-sdk" name = "opentelemetry-sdk"
version = "1.33.0" version = "1.32.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "opentelemetry-api" }, { name = "opentelemetry-api" },
{ name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-semantic-conventions" },
{ name = "typing-extensions" }, { name = "typing-extensions" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/37/0a/b7ae406175a2798a767e12db223e842911d9c398eea100c41c989afd2aa8/opentelemetry_sdk-1.33.0.tar.gz", hash = "sha256:a7fc56d1e07b218fcc316b24d21b59d3f1967b2ca22c217b05da3a26b797cc68", size = 161381, upload-time = "2025-05-09T14:56:12.347Z" } sdist = { url = "https://files.pythonhosted.org/packages/a3/65/2069caef9257fae234ca0040d945c741aa7afbd83a7298ee70fc0bc6b6f4/opentelemetry_sdk-1.32.1.tar.gz", hash = "sha256:8ef373d490961848f525255a42b193430a0637e064dd132fd2a014d94792a092", size = 161044, upload-time = "2025-04-15T16:02:28.905Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/b4/34/831f5d9ae9375c9ba2446cb3cc0be79d8d73b78f813c9567e1615c2624f6/opentelemetry_sdk-1.33.0-py3-none-any.whl", hash = "sha256:bed376b6d37fbf00688bb65edfee817dd01d48b8559212831437529a6066049a", size = 118861, upload-time = "2025-05-09T14:55:56.956Z" }, { url = "https://files.pythonhosted.org/packages/dc/00/d3976cdcb98027aaf16f1e980e54935eb820872792f0eaedd4fd7abb5964/opentelemetry_sdk-1.32.1-py3-none-any.whl", hash = "sha256:bba37b70a08038613247bc42beee5a81b0ddca422c7d7f1b097b32bf1c7e2f17", size = 118989, upload-time = "2025-04-15T16:02:08.814Z" },
] ]
[[package]] [[package]]
name = "opentelemetry-semantic-conventions" name = "opentelemetry-semantic-conventions"
version = "0.54b0" version = "0.53b1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "deprecated" }, { name = "deprecated" },
{ name = "opentelemetry-api" }, { name = "opentelemetry-api" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/92/8c/bc970d1599ff40b7913c953a95195addf11c81a27cc85d5ed568e9f8c57f/opentelemetry_semantic_conventions-0.54b0.tar.gz", hash = "sha256:467b739977bdcb079af1af69f73632535cdb51099d5e3c5709a35d10fe02a9c9", size = 118646, upload-time = "2025-05-09T14:56:13.596Z" } sdist = { url = "https://files.pythonhosted.org/packages/5e/b6/3c56e22e9b51bcb89edab30d54830958f049760bbd9ab0a759cece7bca88/opentelemetry_semantic_conventions-0.53b1.tar.gz", hash = "sha256:4c5a6fede9de61211b2e9fc1e02e8acacce882204cd770177342b6a3be682992", size = 114350, upload-time = "2025-04-15T16:02:29.793Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/aa/f7c46c19aee189e0123ef7209eaafc417e242b2073485dfb40523d6d8612/opentelemetry_semantic_conventions-0.54b0-py3-none-any.whl", hash = "sha256:fad7c1cf8908fd449eb5cf9fbbeefb301acf4bc995101f85277899cec125d823", size = 194937, upload-time = "2025-05-09T14:55:58.562Z" }, { url = "https://files.pythonhosted.org/packages/27/6b/a8fb94760ef8da5ec283e488eb43235eac3ae7514385a51b6accf881e671/opentelemetry_semantic_conventions-0.53b1-py3-none-any.whl", hash = "sha256:21df3ed13f035f8f3ea42d07cbebae37020367a53b47f1ebee3b10a381a00208", size = 188443, upload-time = "2025-04-15T16:02:10.095Z" },
] ]
[[package]] [[package]]
@ -2290,11 +2290,11 @@ wheels = [
[[package]] [[package]]
name = "platformdirs" name = "platformdirs"
version = "4.3.8" version = "4.3.7"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291, upload-time = "2025-03-19T20:36:10.989Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499, upload-time = "2025-03-19T20:36:09.038Z" },
] ]
[[package]] [[package]]
@ -2396,14 +2396,14 @@ wheels = [
[[package]] [[package]]
name = "psycopg" name = "psycopg"
version = "3.2.9" version = "3.2.7"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "tzdata", marker = "sys_platform == 'win32'" }, { name = "tzdata", marker = "sys_platform == 'win32'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/27/4a/93a6ab570a8d1a4ad171a1f4256e205ce48d828781312c0bbaff36380ecb/psycopg-3.2.9.tar.gz", hash = "sha256:2fbb46fcd17bc81f993f28c47f1ebea38d66ae97cc2dbc3cad73b37cefbff700", size = 158122, upload-time = "2025-05-13T16:11:15.533Z" } sdist = { url = "https://files.pythonhosted.org/packages/fe/16/ca27b38762a630b70243f51eb6a728f903a17cddc4961626fa540577aba6/psycopg-3.2.7.tar.gz", hash = "sha256:9afa609c7ebf139827a38c0bf61be9c024a3ed743f56443de9d38e1efc260bf3", size = 157238, upload-time = "2025-04-30T13:05:22.867Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/44/b0/a73c195a56eb6b92e937a5ca58521a5c3346fb233345adc80fd3e2f542e2/psycopg-3.2.9-py3-none-any.whl", hash = "sha256:01a8dadccdaac2123c916208c96e06631641c0566b22005493f09663c7a8d3b6", size = 202705, upload-time = "2025-05-13T16:06:26.584Z" }, { url = "https://files.pythonhosted.org/packages/cb/eb/6e32d259437125a17b0bc2624e06c86149c618501da1dcbc8539b2684f6f/psycopg-3.2.7-py3-none-any.whl", hash = "sha256:d39747d2d5b9658b69fa462ad21d31f1ba4a5722ad1d0cb952552bc0b4125451", size = 200028, upload-time = "2025-04-30T12:59:32.435Z" },
] ]
[package.optional-dependencies] [package.optional-dependencies]
@ -2416,9 +2416,9 @@ pool = [
[[package]] [[package]]
name = "psycopg-c" name = "psycopg-c"
version = "3.2.9" version = "3.2.7"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/83/7f/6147cb842081b0b32692bf5a0fdf58e9ac95418ebac1184d4431ec44b85f/psycopg_c-3.2.9.tar.gz", hash = "sha256:8c9f654f20c6c56bddc4543a3caab236741ee94b6732ab7090b95605502210e2", size = 609538, upload-time = "2025-05-13T16:11:19.856Z" } sdist = { url = "https://files.pythonhosted.org/packages/b2/13/74e41e5195e6a0a02b9f1e3560bc714021b725e89a40f5879df58d4189c6/psycopg_c-3.2.7.tar.gz", hash = "sha256:14455cf71ed29fdfa725c550f8c58056a852bb27b55eb59e3a0f127ca92751a3", size = 609707, upload-time = "2025-04-30T13:05:24.834Z" }
[[package]] [[package]]
name = "psycopg-pool" name = "psycopg-pool"
@ -2874,27 +2874,27 @@ wheels = [
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.11.9" version = "0.11.8"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f5/e7/e55dda1c92cdcf34b677ebef17486669800de01e887b7831a1b8fdf5cb08/ruff-0.11.9.tar.gz", hash = "sha256:ebd58d4f67a00afb3a30bf7d383e52d0e036e6195143c6db7019604a05335517", size = 4132134, upload-time = "2025-05-09T16:19:41.511Z" } sdist = { url = "https://files.pythonhosted.org/packages/52/f6/adcf73711f31c9f5393862b4281c875a462d9f639f4ccdf69dc368311c20/ruff-0.11.8.tar.gz", hash = "sha256:6d742d10626f9004b781f4558154bb226620a7242080e11caeffab1a40e99df8", size = 4086399, upload-time = "2025-05-01T14:53:24.459Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/71/75dfb7194fe6502708e547941d41162574d1f579c4676a8eb645bf1a6842/ruff-0.11.9-py3-none-linux_armv6l.whl", hash = "sha256:a31a1d143a5e6f499d1fb480f8e1e780b4dfdd580f86e05e87b835d22c5c6f8c", size = 10335453, upload-time = "2025-05-09T16:18:58.2Z" }, { url = "https://files.pythonhosted.org/packages/9f/60/c6aa9062fa518a9f86cb0b85248245cddcd892a125ca00441df77d79ef88/ruff-0.11.8-py3-none-linux_armv6l.whl", hash = "sha256:896a37516c594805e34020c4a7546c8f8a234b679a7716a3f08197f38913e1a3", size = 10272473, upload-time = "2025-05-01T14:52:37.252Z" },
{ url = "https://files.pythonhosted.org/packages/74/fc/ad80c869b1732f53c4232bbf341f33c5075b2c0fb3e488983eb55964076a/ruff-0.11.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:66bc18ca783b97186a1f3100e91e492615767ae0a3be584e1266aa9051990722", size = 11072566, upload-time = "2025-05-09T16:19:01.432Z" }, { url = "https://files.pythonhosted.org/packages/a0/e4/0325e50d106dc87c00695f7bcd5044c6d252ed5120ebf423773e00270f50/ruff-0.11.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ab86d22d3d721a40dd3ecbb5e86ab03b2e053bc93c700dc68d1c3346b36ce835", size = 11040862, upload-time = "2025-05-01T14:52:41.022Z" },
{ url = "https://files.pythonhosted.org/packages/87/0d/0ccececef8a0671dae155cbf7a1f90ea2dd1dba61405da60228bbe731d35/ruff-0.11.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:bd576cd06962825de8aece49f28707662ada6a1ff2db848d1348e12c580acbf1", size = 10435020, upload-time = "2025-05-09T16:19:03.897Z" }, { url = "https://files.pythonhosted.org/packages/e6/27/b87ea1a7be37fef0adbc7fd987abbf90b6607d96aa3fc67e2c5b858e1e53/ruff-0.11.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:258f3585057508d317610e8a412788cf726efeefa2fec4dba4001d9e6f90d46c", size = 10385273, upload-time = "2025-05-01T14:52:43.551Z" },
{ url = "https://files.pythonhosted.org/packages/52/01/e249e1da6ad722278094e183cbf22379a9bbe5f21a3e46cef24ccab76e22/ruff-0.11.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b1d18b4be8182cc6fddf859ce432cc9631556e9f371ada52f3eaefc10d878de", size = 10593935, upload-time = "2025-05-09T16:19:06.455Z" }, { url = "https://files.pythonhosted.org/packages/d3/f7/3346161570d789045ed47a86110183f6ac3af0e94e7fd682772d89f7f1a1/ruff-0.11.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:727d01702f7c30baed3fc3a34901a640001a2828c793525043c29f7614994a8c", size = 10578330, upload-time = "2025-05-01T14:52:45.48Z" },
{ url = "https://files.pythonhosted.org/packages/ed/9a/40cf91f61e3003fe7bd43f1761882740e954506c5a0f9097b1cff861f04c/ruff-0.11.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0f3f46f759ac623e94824b1e5a687a0df5cd7f5b00718ff9c24f0a894a683be7", size = 10172971, upload-time = "2025-05-09T16:19:10.261Z" }, { url = "https://files.pythonhosted.org/packages/c6/c3/327fb950b4763c7b3784f91d3038ef10c13b2d42322d4ade5ce13a2f9edb/ruff-0.11.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3dca977cc4fc8f66e89900fa415ffe4dbc2e969da9d7a54bfca81a128c5ac219", size = 10122223, upload-time = "2025-05-01T14:52:47.675Z" },
{ url = "https://files.pythonhosted.org/packages/61/12/d395203de1e8717d7a2071b5a340422726d4736f44daf2290aad1085075f/ruff-0.11.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f34847eea11932d97b521450cf3e1d17863cfa5a94f21a056b93fb86f3f3dba2", size = 11748631, upload-time = "2025-05-09T16:19:12.307Z" }, { url = "https://files.pythonhosted.org/packages/de/c7/ba686bce9adfeb6c61cb1bbadc17d58110fe1d602f199d79d4c880170f19/ruff-0.11.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c657fa987d60b104d2be8b052d66da0a2a88f9bd1d66b2254333e84ea2720c7f", size = 11697353, upload-time = "2025-05-01T14:52:50.264Z" },
{ url = "https://files.pythonhosted.org/packages/66/d6/ef4d5eba77677eab511644c37c55a3bb8dcac1cdeb331123fe342c9a16c9/ruff-0.11.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f33b15e00435773df97cddcd263578aa83af996b913721d86f47f4e0ee0ff271", size = 12409236, upload-time = "2025-05-09T16:19:15.006Z" }, { url = "https://files.pythonhosted.org/packages/53/8e/a4fb4a1ddde3c59e73996bb3ac51844ff93384d533629434b1def7a336b0/ruff-0.11.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f2e74b021d0de5eceb8bd32919f6ff8a9b40ee62ed97becd44993ae5b9949474", size = 12375936, upload-time = "2025-05-01T14:52:52.394Z" },
{ url = "https://files.pythonhosted.org/packages/c5/8f/5a2c5fc6124dd925a5faf90e1089ee9036462118b619068e5b65f8ea03df/ruff-0.11.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b27613a683b086f2aca8996f63cb3dd7bc49e6eccf590563221f7b43ded3f65", size = 11881436, upload-time = "2025-05-09T16:19:17.063Z" }, { url = "https://files.pythonhosted.org/packages/ad/a1/9529cb1e2936e2479a51aeb011307e7229225df9ac64ae064d91ead54571/ruff-0.11.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9b5ef39820abc0f2c62111f7045009e46b275f5b99d5e59dda113c39b7f4f38", size = 11850083, upload-time = "2025-05-01T14:52:55.424Z" },
{ url = "https://files.pythonhosted.org/packages/39/d1/9683f469ae0b99b95ef99a56cfe8c8373c14eba26bd5c622150959ce9f64/ruff-0.11.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e0d88756e63e8302e630cee3ce2ffb77859797cc84a830a24473939e6da3ca6", size = 13982759, upload-time = "2025-05-09T16:19:19.693Z" }, { url = "https://files.pythonhosted.org/packages/3e/94/8f7eac4c612673ae15a4ad2bc0ee62e03c68a2d4f458daae3de0e47c67ba/ruff-0.11.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1dba3135ca503727aa4648152c0fa67c3b1385d3dc81c75cd8a229c4b2a1458", size = 14005834, upload-time = "2025-05-01T14:52:58.056Z" },
{ url = "https://files.pythonhosted.org/packages/4e/0b/c53a664f06e0faab596397867c6320c3816df479e888fe3af63bc3f89699/ruff-0.11.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:537c82c9829d7811e3aa680205f94c81a2958a122ac391c0eb60336ace741a70", size = 11541985, upload-time = "2025-05-09T16:19:21.831Z" }, { url = "https://files.pythonhosted.org/packages/1e/7c/6f63b46b2be870cbf3f54c9c4154d13fac4b8827f22fa05ac835c10835b2/ruff-0.11.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f024d32e62faad0f76b2d6afd141b8c171515e4fb91ce9fd6464335c81244e5", size = 11503713, upload-time = "2025-05-01T14:53:01.244Z" },
{ url = "https://files.pythonhosted.org/packages/23/a0/156c4d7e685f6526a636a60986ee4a3c09c8c4e2a49b9a08c9913f46c139/ruff-0.11.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:440ac6a7029f3dee7d46ab7de6f54b19e34c2b090bb4f2480d0a2d635228f381", size = 10465775, upload-time = "2025-05-09T16:19:24.401Z" }, { url = "https://files.pythonhosted.org/packages/3a/91/57de411b544b5fe072779678986a021d87c3ee5b89551f2ca41200c5d643/ruff-0.11.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d365618d3ad747432e1ae50d61775b78c055fee5936d77fb4d92c6f559741948", size = 10457182, upload-time = "2025-05-01T14:53:03.726Z" },
{ url = "https://files.pythonhosted.org/packages/43/d5/88b9a6534d9d4952c355e38eabc343df812f168a2c811dbce7d681aeb404/ruff-0.11.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:71c539bac63d0788a30227ed4d43b81353c89437d355fdc52e0cda4ce5651787", size = 10170957, upload-time = "2025-05-09T16:19:27.08Z" }, { url = "https://files.pythonhosted.org/packages/01/49/cfe73e0ce5ecdd3e6f1137bf1f1be03dcc819d1bfe5cff33deb40c5926db/ruff-0.11.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4d9aaa91035bdf612c8ee7266153bcf16005c7c7e2f5878406911c92a31633cb", size = 10101027, upload-time = "2025-05-01T14:53:06.555Z" },
{ url = "https://files.pythonhosted.org/packages/f0/b8/2bd533bdaf469dc84b45815ab806784d561fab104d993a54e1852596d581/ruff-0.11.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c67117bc82457e4501473c5f5217d49d9222a360794bfb63968e09e70f340abd", size = 11143307, upload-time = "2025-05-09T16:19:29.462Z" }, { url = "https://files.pythonhosted.org/packages/56/21/a5cfe47c62b3531675795f38a0ef1c52ff8de62eaddf370d46634391a3fb/ruff-0.11.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0eba551324733efc76116d9f3a0d52946bc2751f0cd30661564117d6fd60897c", size = 11111298, upload-time = "2025-05-01T14:53:08.825Z" },
{ url = "https://files.pythonhosted.org/packages/2f/d9/43cfba291788459b9bfd4e09a0479aa94d05ab5021d381a502d61a807ec1/ruff-0.11.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e4b78454f97aa454586e8a5557facb40d683e74246c97372af3c2d76901d697b", size = 11603026, upload-time = "2025-05-09T16:19:31.569Z" }, { url = "https://files.pythonhosted.org/packages/36/98/f76225f87e88f7cb669ae92c062b11c0a1e91f32705f829bd426f8e48b7b/ruff-0.11.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:161eb4cff5cfefdb6c9b8b3671d09f7def2f960cee33481dd898caf2bcd02304", size = 11566884, upload-time = "2025-05-01T14:53:11.626Z" },
{ url = "https://files.pythonhosted.org/packages/22/e6/7ed70048e89b01d728ccc950557a17ecf8df4127b08a56944b9d0bae61bc/ruff-0.11.9-py3-none-win32.whl", hash = "sha256:7fe1bc950e7d7b42caaee2a8a3bc27410547cc032c9558ee2e0f6d3b209e845a", size = 10548627, upload-time = "2025-05-09T16:19:33.657Z" }, { url = "https://files.pythonhosted.org/packages/de/7e/fff70b02e57852fda17bd43f99dda37b9bcf3e1af3d97c5834ff48d04715/ruff-0.11.8-py3-none-win32.whl", hash = "sha256:5b18caa297a786465cc511d7f8be19226acf9c0a1127e06e736cd4e1878c3ea2", size = 10451102, upload-time = "2025-05-01T14:53:14.303Z" },
{ url = "https://files.pythonhosted.org/packages/90/36/1da5d566271682ed10f436f732e5f75f926c17255c9c75cefb77d4bf8f10/ruff-0.11.9-py3-none-win_amd64.whl", hash = "sha256:52edaa4a6d70f8180343a5b7f030c7edd36ad180c9f4d224959c2d689962d964", size = 11634340, upload-time = "2025-05-09T16:19:35.815Z" }, { url = "https://files.pythonhosted.org/packages/7b/a9/eaa571eb70648c9bde3120a1d5892597de57766e376b831b06e7c1e43945/ruff-0.11.8-py3-none-win_amd64.whl", hash = "sha256:6e70d11043bef637c5617297bdedec9632af15d53ac1e1ba29c448da9341b0c4", size = 11597410, upload-time = "2025-05-01T14:53:16.571Z" },
{ url = "https://files.pythonhosted.org/packages/40/f7/70aad26e5877c8f7ee5b161c4c9fa0100e63fc4c944dc6d97b9c7e871417/ruff-0.11.9-py3-none-win_arm64.whl", hash = "sha256:bcf42689c22f2e240f496d0c183ef2c6f7b35e809f12c1db58f75d9aa8d630ca", size = 10741080, upload-time = "2025-05-09T16:19:39.605Z" }, { url = "https://files.pythonhosted.org/packages/cd/be/f6b790d6ae98f1f32c645f8540d5c96248b72343b0a56fab3a07f2941897/ruff-0.11.8-py3-none-win_arm64.whl", hash = "sha256:304432e4c4a792e3da85b7699feb3426a0908ab98bf29df22a31b0cdd098fac2", size = 10713129, upload-time = "2025-05-01T14:53:22.27Z" },
] ]
[[package]] [[package]]
@ -2940,15 +2940,15 @@ wheels = [
[[package]] [[package]]
name = "sentry-sdk" name = "sentry-sdk"
version = "2.28.0" version = "2.27.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "certifi" }, { name = "certifi" },
{ name = "urllib3" }, { name = "urllib3" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/5e/bb/6a41b2e0e9121bed4d2ec68d50568ab95c49f4744156a9bbb789c866c66d/sentry_sdk-2.28.0.tar.gz", hash = "sha256:14d2b73bc93afaf2a9412490329099e6217761cbab13b6ee8bc0e82927e1504e", size = 325052, upload-time = "2025-05-12T07:53:12.785Z" } sdist = { url = "https://files.pythonhosted.org/packages/cf/b6/a92ae6fa6d7e6e536bc586776b1669b84fb724dfe21b8ff08297f2d7c969/sentry_sdk-2.27.0.tar.gz", hash = "sha256:90f4f883f9eff294aff59af3d58c2d1b64e3927b28d5ada2b9b41f5aeda47daf", size = 323556, upload-time = "2025-04-24T10:09:37.927Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/9b/4e/b1575833094c088dfdef63fbca794518860fcbc8002aadf51ebe8b6a387f/sentry_sdk-2.28.0-py2.py3-none-any.whl", hash = "sha256:51496e6cb3cb625b99c8e08907c67a9112360259b0ef08470e532c3ab184a232", size = 341693, upload-time = "2025-05-12T07:53:10.882Z" }, { url = "https://files.pythonhosted.org/packages/dd/8b/fb496a45854e37930b57564a20fb8e90dd0f8b6add0491527c00f2163b00/sentry_sdk-2.27.0-py2.py3-none-any.whl", hash = "sha256:c58935bfff8af6a0856d37e8adebdbc7b3281c2b632ec823ef03cd108d216ff0", size = 340786, upload-time = "2025-04-24T10:09:35.897Z" },
] ]
[[package]] [[package]]
@ -3000,11 +3000,11 @@ wheels = [
[[package]] [[package]]
name = "setuptools" name = "setuptools"
version = "80.4.0" version = "80.3.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/95/32/0cc40fe41fd2adb80a2f388987f4f8db3c866c69e33e0b4c8b093fdf700e/setuptools-80.4.0.tar.gz", hash = "sha256:5a78f61820bc088c8e4add52932ae6b8cf423da2aff268c23f813cfbb13b4006", size = 1315008, upload-time = "2025-05-09T20:42:27.972Z" } sdist = { url = "https://files.pythonhosted.org/packages/70/dc/3976b322de9d2e87ed0007cf04cc7553969b6c7b3f48a565d0333748fbcd/setuptools-80.3.1.tar.gz", hash = "sha256:31e2c58dbb67c99c289f51c16d899afedae292b978f8051efaf6262d8212f927", size = 1315082, upload-time = "2025-05-04T18:47:04.397Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/b1/93/dba5ed08c2e31ec7cdc2ce75705a484ef0be1a2fecac8a58272489349de8/setuptools-80.4.0-py3-none-any.whl", hash = "sha256:6cdc8cb9a7d590b237dbe4493614a9b75d0559b888047c1f67d49ba50fc3edb2", size = 1200812, upload-time = "2025-05-09T20:42:25.325Z" }, { url = "https://files.pythonhosted.org/packages/53/7e/5d8af3317ddbf9519b687bd1c39d8737fde07d97f54df65553faca5cffb1/setuptools-80.3.1-py3-none-any.whl", hash = "sha256:ea8e00d7992054c4c592aeb892f6ad51fe1b4d90cc6947cc45c45717c40ec537", size = 1201172, upload-time = "2025-05-04T18:47:02.575Z" },
] ]
[[package]] [[package]]
@ -3099,14 +3099,14 @@ wheels = [
[[package]] [[package]]
name = "tenant-schemas-celery" name = "tenant-schemas-celery"
version = "4.0.1" version = "3.0.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "celery" }, { name = "celery" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/19/f8/cf055bf171b5d83d6fe96f1840fba90d3d274be2b5c35cd21b873302b128/tenant_schemas_celery-4.0.1.tar.gz", hash = "sha256:8b8f055fcd82aa53274c09faf88653a935241518d93b86ab2d43a3df3b70c7f8", size = 18870, upload-time = "2025-04-22T18:23:51.061Z" } sdist = { url = "https://files.pythonhosted.org/packages/d0/fe/cfe19eb7cc3ad8e39d7df7b7c44414bf665b6ac6660c998eb498f89d16c6/tenant_schemas_celery-3.0.0.tar.gz", hash = "sha256:6be3ae1a5826f262f0f3dd343c6a85a34a1c59b89e04ae37de018f36562fed55", size = 15954, upload-time = "2024-05-19T11:16:41.837Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/a8/fd663c461550d6fedfb24e987acc1557ae5b6615ca08fc6c70dbaaa88aa5/tenant_schemas_celery-4.0.1-py3-none-any.whl", hash = "sha256:d06a3ff6956db3a95168ce2051b7bff2765f9ce0d070e14df92f07a2b60ae0a0", size = 21364, upload-time = "2025-04-22T18:23:49.899Z" }, { url = "https://files.pythonhosted.org/packages/db/2c/376e1e641ad08b374c75d896468a7be2e6906ce3621fd0c9f9dc09ff1963/tenant_schemas_celery-3.0.0-py3-none-any.whl", hash = "sha256:ca0f69e78ef698eb4813468231df5a0ab6a660c08e657b65f5ac92e16887eec8", size = 18108, upload-time = "2024-05-19T11:16:39.92Z" },
] ]
[[package]] [[package]]
@ -3160,7 +3160,7 @@ wheels = [
[[package]] [[package]]
name = "twilio" name = "twilio"
version = "9.6.1" version = "9.6.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "aiohttp" }, { name = "aiohttp" },
@ -3168,9 +3168,9 @@ dependencies = [
{ name = "pyjwt" }, { name = "pyjwt" },
{ name = "requests" }, { name = "requests" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/95/78/453ff0d35442c53490c22d077f580684a2352846c721d3e01f4c6dfa85bd/twilio-9.6.1.tar.gz", hash = "sha256:bb80b31d4d9e55c33872efef7fb99373149ed4093f21c56cf582797da45862f5", size = 987002, upload-time = "2025-05-13T09:56:55.183Z" } sdist = { url = "https://files.pythonhosted.org/packages/91/e9/ffc6e52465ffc16fad31fa64aea4e10e06cb4803447310c539c6fd66e859/twilio-9.6.0.tar.gz", hash = "sha256:bcb6cbc7f1dad09717d48d3e610573b6a55fa4a1f6fd1006f5b59cf6878b5562", size = 986499, upload-time = "2025-05-05T10:48:17.921Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/02/f4/36fe2566a3ad7f71a89fd28ea2ebb6b2aa05c3a4d5a55b3ca6c358768c6b/twilio-9.6.1-py2.py3-none-any.whl", hash = "sha256:441fdab61b9a204eef770368380b962cbf08dc0fe9f757fe4b1d63ced37ddeed", size = 1859407, upload-time = "2025-05-13T09:56:53.094Z" }, { url = "https://files.pythonhosted.org/packages/b5/04/1d9f452b1089c634bd6d64b40b9002c935b8214e9b08a7cbbfef204c8186/twilio-9.6.0-py2.py3-none-any.whl", hash = "sha256:19e8554c56324186973dcb3121de34626755db15331767e3021a2e23f80c6a3b", size = 1859151, upload-time = "2025-05-05T10:48:15.394Z" },
] ]
[[package]] [[package]]
@ -3487,17 +3487,12 @@ wheels = [
[[package]] [[package]]
name = "xmlsec" name = "xmlsec"
version = "1.3.15" version = "1.3.14"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "lxml" }, { name = "lxml" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/6b/0b/d851367799b865500efd0b255c39fc5d30892ea28c1569ca185a76d19576/xmlsec-1.3.15.tar.gz", hash = "sha256:baa856b83d0012e278e6f6cbec96ac8128de667ca9fa9a2eeb02c752e816f6d8", size = 114117, upload-time = "2025-03-11T22:37:00.567Z" } sdist = { url = "https://files.pythonhosted.org/packages/25/5b/244459b51dfe91211c1d9ec68fb5307dfc51e014698f52de575d25f753e0/xmlsec-1.3.14.tar.gz", hash = "sha256:934f804f2f895bcdb86f1eaee236b661013560ee69ec108d29cdd6e5f292a2d9", size = 68854, upload-time = "2024-04-17T19:34:29.388Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/13/17/0a272e6087ddb24bec96528acf061341845f458671e2a5cb35ff867a7c89/xmlsec-1.3.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6ac2154311d32a6571e22f224ed16356029e59bd5ca76edeb3922a809adfe89c", size = 3746315, upload-time = "2025-03-11T22:36:43.675Z" },
{ url = "https://files.pythonhosted.org/packages/b7/91/7ce9317e3a2a03e3811e62be52e091c1e661da2d59b5c7f60ec1840a1e6b/xmlsec-1.3.15-cp313-cp313-win32.whl", hash = "sha256:5ed218129f89b0592926ad2be42c017bece469db9b7380dc41bc09b01ca26d5d", size = 2146158, upload-time = "2025-03-11T22:36:44.887Z" },
{ url = "https://files.pythonhosted.org/packages/3d/e0/93311b9eedc11055ba667e666dc6ca1e2cc59c2356e91b73c3d5a6738fbf/xmlsec-1.3.15-cp313-cp313-win_amd64.whl", hash = "sha256:5fc29e69b064323317b3862751a3a8107670e0a17510ca4517bbdc1939a90b1a", size = 2442027, upload-time = "2025-03-11T22:36:46.431Z" },
]
[[package]] [[package]]
name = "yarl" name = "yarl"

View File

@ -0,0 +1,11 @@
import { create } from "@storybook/theming/create";
const isDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches;
export default create({
base: isDarkMode ? "dark" : "light",
brandTitle: "authentik Storybook",
brandUrl: "https://goauthentik.io",
brandImage: "https://goauthentik.io/img/icon_left_brand_colour.svg",
brandTarget: "_self",
});

View File

@ -1,63 +0,0 @@
/**
* @file Storybook configuration.
* @import { StorybookConfig } from "@storybook/web-components-vite";
* @import { InlineConfig, Plugin } from "vite";
*/
import { createBundleDefinitions } from "@goauthentik/web/scripts/esbuild/environment";
import postcssLit from "rollup-plugin-postcss-lit";
import tsconfigPaths from "vite-tsconfig-paths";
const CSSImportPattern = /import [\w$]+ from .+\.(css)/g;
const JavaScriptFilePattern = /\.m?(js|ts|tsx)$/;
/**
* @satisfies {Plugin<never>}
*/
const inlineCSSPlugin = {
name: "inline-css-plugin",
transform: (source, id) => {
if (!JavaScriptFilePattern.test(id)) return;
const code = source.replace(CSSImportPattern, (match) => {
return `${match}?inline`;
});
return {
code,
};
},
};
/**
* @satisfies {StorybookConfig}
*/
const config = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
addons: [
"@storybook/addon-controls",
"@storybook/addon-links",
"@storybook/addon-essentials",
"storybook-addon-mock",
],
framework: {
name: "@storybook/web-components-vite",
options: {},
},
docs: {
autodocs: "tag",
},
viteFinal({ plugins = [], ...config }) {
/**
* @satisfies {InlineConfig}
*/
const mergedConfig = {
...config,
define: createBundleDefinitions(),
plugins: [inlineCSSPlugin, ...plugins, postcssLit(), tsconfigPaths()],
};
return mergedConfig;
},
};
export default config;

81
web/.storybook/main.ts Normal file
View File

@ -0,0 +1,81 @@
import replace from "@rollup/plugin-replace";
import type { StorybookConfig } from "@storybook/web-components-vite";
import { cwd } from "process";
import modify from "rollup-plugin-modify";
import postcssLit from "rollup-plugin-postcss-lit";
import tsconfigPaths from "vite-tsconfig-paths";
export const isProdBuild = process.env.NODE_ENV === "production";
export const apiBasePath = process.env.AK_API_BASE_PATH || "";
const importInlinePatterns = [
'import AKGlobal from "(\\.\\./)*common/styles/authentik\\.css',
'import AKGlobal from "@goauthentik/common/styles/authentik\\.css',
'import PF.+ from "@patternfly/patternfly/\\S+\\.css',
'import ThemeDark from "@goauthentik/common/styles/theme-dark\\.css',
'import OneDark from "@goauthentik/common/styles/one-dark\\.css',
'import styles from "\\./LibraryPageImpl\\.css',
];
const importInlineRegexp = new RegExp(importInlinePatterns.map((a) => `(${a})`).join("|"));
const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
addons: [
"@storybook/addon-controls",
"@storybook/addon-links",
"@storybook/addon-essentials",
"storybook-addon-mock",
],
staticDirs: [
{
from: "../node_modules/@patternfly/patternfly/patternfly-base.css",
to: "@patternfly/patternfly/patternfly-base.css",
},
{
from: "../src/common/styles/authentik.css",
to: "@goauthentik/common/styles/authentik.css",
},
{
from: "../src/common/styles/theme-dark.css",
to: "@goauthentik/common/styles/theme-dark.css",
},
{
from: "../src/common/styles/one-dark.css",
to: "@goauthentik/common/styles/one-dark.css",
},
],
framework: {
name: "@storybook/web-components-vite",
options: {},
},
docs: {
autodocs: "tag",
},
async viteFinal(config) {
return {
...config,
plugins: [
modify({
find: importInlineRegexp,
replace: (match: RegExpMatchArray) => {
return `${match}?inline`;
},
}),
replace({
"process.env.NODE_ENV": JSON.stringify(
isProdBuild ? "production" : "development",
),
"process.env.CWD": JSON.stringify(cwd()),
"process.env.AK_API_BASE_PATH": JSON.stringify(apiBasePath),
"preventAssignment": true,
}),
...config.plugins,
postcssLit(),
tsconfigPaths(),
],
};
},
};
export default config;

View File

@ -1,38 +0,0 @@
/**
* @file Storybook manager configuration.
*
* @import { ThemeVarsPartial } from "storybook/internal/theming";
*/
import { createUIThemeEffect, resolveUITheme } from "@goauthentik/web/common/theme.ts";
import { addons } from "@storybook/manager-api";
import { create } from "@storybook/theming/create";
/**
* @satisfies {Partial<ThemeVarsPartial>}
*/
const baseTheme = {
brandTitle: "authentik Storybook",
brandUrl: "https://goauthentik.io",
brandImage: "https://goauthentik.io/img/icon_left_brand_colour.svg",
brandTarget: "_self",
};
const uiTheme = resolveUITheme();
addons.setConfig({
theme: create({
...baseTheme,
base: uiTheme,
}),
enableShortcuts: false,
});
createUIThemeEffect((nextUITheme) => {
addons.setConfig({
theme: create({
...baseTheme,
base: nextUITheme,
}),
enableShortcuts: false,
});
});

View File

@ -0,0 +1,9 @@
// .storybook/manager.js
import { addons } from "@storybook/manager-api";
import authentikTheme from "./authentikTheme";
addons.setConfig({
theme: authentikTheme,
enableShortcuts: false,
});

View File

@ -1,3 +1,5 @@
<link rel="stylesheet" href="@patternfly/patternfly/patternfly-base.css" />
<link rel="stylesheet" href="@goauthentik/common/styles/authentik.css" />
<style> <style>
body { body {
overflow-y: scroll; overflow-y: scroll;

View File

@ -1,32 +0,0 @@
/// <reference types="../types/css.js" />
/**
* @file Storybook manager configuration.
*
* @import { Preview } from "@storybook/web-components";
*/
import { applyDocumentTheme } from "@goauthentik/web/common/theme.ts";
applyDocumentTheme();
/**
* @satisfies {Preview}
*/
const preview = {
parameters: {
options: {
storySort: {
method: "alphabetical",
},
},
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
};
export default preview;

30
web/.storybook/preview.ts Normal file
View File

@ -0,0 +1,30 @@
import type { Preview } from "@storybook/web-components";
import "@goauthentik/common/styles/authentik.css";
// import "@goauthentik/common/styles/theme-dark.css";
import "@patternfly/patternfly/components/Brand/brand.css";
import "@patternfly/patternfly/components/Page/page.css";
// .storybook/preview.js
import "@patternfly/patternfly/patternfly-base.css";
const preview: Preview = {
parameters: {
options: {
storySort: {
method: "alphabetical",
},
},
actions: { argTypesRegex: "^on[A-Z].*" },
cssUserPrefs: {
"prefers-color-scheme": "light",
},
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
};
export default preview;

2780
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -19,6 +19,7 @@
"lint:precommit": "wireit", "lint:precommit": "wireit",
"lint:types": "wireit", "lint:types": "wireit",
"lit-analyse": "wireit", "lit-analyse": "wireit",
"postinstall": "bash scripts/patch-spotlight.sh",
"precommit": "wireit", "precommit": "wireit",
"prettier": "wireit", "prettier": "wireit",
"prettier-check": "wireit", "prettier-check": "wireit",
@ -36,14 +37,7 @@
"exports": { "exports": {
"./package.json": "./package.json", "./package.json": "./package.json",
"./paths": "./paths.js", "./paths": "./paths.js",
"./scripts/*": "./scripts/*.mjs", "./scripts/*": "./scripts/*.mjs"
"./elements/*": "./src/elements/*",
"./common/*": "./src/common/*",
"./components/*": "./src/components/*",
"./flow/*": "./src/flow/*",
"./locales/*": "./src/locales/*",
"./user/*": "./src/user/*",
"./admin/*": "./src/admin/*"
}, },
"dependencies": { "dependencies": {
"@codemirror/lang-css": "^6.3.1", "@codemirror/lang-css": "^6.3.1",
@ -56,7 +50,7 @@
"@floating-ui/dom": "^1.6.11", "@floating-ui/dom": "^1.6.11",
"@formatjs/intl-listformat": "^7.5.7", "@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.6.0", "@fortawesome/fontawesome-free": "^6.6.0",
"@goauthentik/api": "^2025.4.1-1747332783", "@goauthentik/api": "^2025.4.0-1746018955",
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2", "@lit/localize": "^0.12.2",
"@lit/reactive-element": "^2.0.4", "@lit/reactive-element": "^2.0.4",
@ -112,14 +106,14 @@
"@hcaptcha/types": "^1.0.4", "@hcaptcha/types": "^1.0.4",
"@lit/localize-tools": "^0.8.0", "@lit/localize-tools": "^0.8.0",
"@rollup/plugin-replace": "^6.0.1", "@rollup/plugin-replace": "^6.0.1",
"@storybook/addon-essentials": "^8.6.12", "@storybook/addon-essentials": "^8.3.4",
"@storybook/addon-links": "^8.6.12", "@storybook/addon-links": "^8.3.4",
"@storybook/blocks": "^8.6.12", "@storybook/api": "^7.6.17",
"@storybook/experimental-addon-test": "^8.6.12", "@storybook/blocks": "^8.3.4",
"@storybook/manager-api": "^8.6.12", "@storybook/builder-vite": "^8.3.4",
"@storybook/test": "^8.6.12", "@storybook/manager-api": "^8.3.4",
"@storybook/web-components": "^8.6.12", "@storybook/web-components": "^8.3.4",
"@storybook/web-components-vite": "^8.6.12", "@storybook/web-components-vite": "^8.3.4",
"@trivago/prettier-plugin-sort-imports": "^5.2.2", "@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/chart.js": "^2.9.41", "@types/chart.js": "^2.9.41",
"@types/codemirror": "^5.60.15", "@types/codemirror": "^5.60.15",
@ -151,8 +145,9 @@
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"pseudolocale": "^2.1.0", "pseudolocale": "^2.1.0",
"rollup-plugin-modify": "^3.0.0",
"rollup-plugin-postcss-lit": "^2.1.0", "rollup-plugin-postcss-lit": "^2.1.0",
"storybook": "^8.6.12", "storybook": "^8.3.4",
"storybook-addon-mock": "^5.0.0", "storybook-addon-mock": "^5.0.0",
"turnstile-types": "^1.2.3", "turnstile-types": "^1.2.3",
"typescript": "^5.6.2", "typescript": "^5.6.2",

View File

@ -6,7 +6,7 @@
* @import { Message as ESBuildMessage } from "esbuild"; * @import { Message as ESBuildMessage } from "esbuild";
*/ */
const logPrefix = "authentik/dev/web: "; const logPrefix = "👷 [ESBuild]";
const log = console.debug.bind(console, logPrefix); const log = console.debug.bind(console, logPrefix);
/** /**
@ -21,7 +21,7 @@ const log = console.debug.bind(console, logPrefix);
* ESBuild may tree-shake it out of production builds. * ESBuild may tree-shake it out of production builds.
* *
* ```ts * ```ts
* if (import.meta.env.NODE_ENV=== "development") { * if (process.env.NODE_ENV === "development") {
* await import("@goauthentik/esbuild-plugin-live-reload/client") * await import("@goauthentik/esbuild-plugin-live-reload/client")
* .catch(() => console.warn("Failed to import watcher")) * .catch(() => console.warn("Failed to import watcher"))
* } * }
@ -76,7 +76,7 @@ export class ESBuildObserver extends EventSource {
*/ */
#startListener = () => { #startListener = () => {
this.#trackActivity(); this.#trackActivity();
log("⏰ Build started..."); log("⏰ Build started...");
}; };
#internalErrorListener = () => { #internalErrorListener = () => {
@ -86,7 +86,7 @@ export class ESBuildObserver extends EventSource {
clearTimeout(this.#keepAliveInterval); clearTimeout(this.#keepAliveInterval);
this.close(); this.close();
log("⛔️ Closing connection"); log("⛔️ Closing connection");
} }
}; };
@ -126,13 +126,13 @@ export class ESBuildObserver extends EventSource {
this.#trackActivity(); this.#trackActivity();
if (!this.online) { if (!this.online) {
log("🚫 Build finished while offline."); log("🚫 Build finished while offline.");
this.deferredReload = true; this.deferredReload = true;
return; return;
} }
log("🛎️ Build completed! Reloading..."); log("🛎️ Build completed! Reloading...");
// We use an animation frame to keep the reload from happening before the // We use an animation frame to keep the reload from happening before the
// event loop has a chance to process the message. // event loop has a chance to process the message.
@ -189,13 +189,13 @@ export class ESBuildObserver extends EventSource {
if (!this.deferredReload) return; if (!this.deferredReload) return;
log("🛎️ Reloading after offline build..."); log("🛎️ Reloading after offline build...");
this.deferredReload = false; this.deferredReload = false;
window.location.reload(); window.location.reload();
}); });
log("🛎️ Listening for build changes..."); log("🛎️ Listening for build changes...");
this.#keepAliveInterval = setInterval(() => { this.#keepAliveInterval = setInterval(() => {
const now = Date.now(); const now = Date.now();
@ -203,7 +203,7 @@ export class ESBuildObserver extends EventSource {
if (now - this.lastUpdatedAt < 10_000) return; if (now - this.lastUpdatedAt < 10_000) return;
this.alive = false; this.alive = false;
log("👋 Waiting for build to start..."); log("👋 Waiting for build to start...");
}, 15_000); }, 15_000);
} }

View File

@ -4,20 +4,15 @@
export {}; export {};
declare global { declare global {
/**
* Environment variables injected by ESBuild.
*/
interface ImportMetaEnv {
/**
* The injected watcher URL for ESBuild.
* This is used for live reloading in development mode.
*
* @format url
*/
readonly ESBUILD_WATCHER_URL?: string;
}
interface ImportMeta { interface ImportMeta {
readonly env: ImportMetaEnv; readonly env: {
/**
* The injected watcher URL for ESBuild.
* This is used for live reloading in development mode.
*
* @format url
*/
ESBUILD_WATCHER_URL: string;
};
} }
} }

View File

@ -1,20 +1,6 @@
/** /**
* @file Utility functions for working with environment variables. * @file Utility functions for building and copying files.
*/ */
/// <reference types="./types/global.js" />
//#region Constants
/**
* The current Node.js environment, defaulting to "development" when not set.
*
* Note, this should only be used during the build process.
*
* If you need to check the environment at runtime, use `process.env.NODE_ENV` to
* ensure that module tree-shaking works correctly.
*
*/
export const NodeEnvironment = process.env.NODE_ENV || "development";
/** /**
* A source environment variable, which can be a string, number, boolean, null, or undefined. * A source environment variable, which can be a string, number, boolean, null, or undefined.
@ -28,26 +14,19 @@ export const NodeEnvironment = process.env.NODE_ENV || "development";
* @typedef {T extends string ? `"${T}"` : T} JSONify * @typedef {T extends string ? `"${T}"` : T} JSONify
*/ */
//#endregion
//#region Utilities
/** /**
* Given an object of environment variables, returns a new object with the same keys and values, but * Given an object of environment variables, returns a new object with the same keys and values, but
* with the values serialized as strings. * with the values serialized as strings.
* *
* @template {Record<string, EnvironmentVariable>} EnvRecord * @template {Record<string, EnvironmentVariable>} EnvRecord
* @template {string} [Prefix='import.meta.env.'] * @template {string} [Prefix='process.env.']
* *
* @param {EnvRecord} input * @param {EnvRecord} input
* @param {Prefix} [prefix='import.meta.env.'] * @param {Prefix} [prefix='process.env.']
* *
* @returns {{[K in keyof EnvRecord as `${Prefix}${K}`]: JSONify<EnvRecord[K]>}} * @returns {{[K in keyof EnvRecord as `${Prefix}${K}`]: JSONify<EnvRecord[K]>}}
*/ */
export function serializeEnvironmentVars( export function serializeEnvironmentVars(input, prefix = /** @type {Prefix} */ ("process.env.")) {
input,
prefix = /** @type {Prefix} */ ("import.meta.env."),
) {
/** /**
* @type {Record<string, string>} * @type {Record<string, string>}
*/ */
@ -61,5 +40,3 @@ export function serializeEnvironmentVars(
return /** @type {any} */ (env); return /** @type {any} */ (env);
} }
//#endregion

View File

@ -0,0 +1,16 @@
/**
* @file Constants for JavaScript and TypeScript files.
*/
/// <reference types="../../types/global.js" />
/**
* The current Node.js environment, defaulting to "development" when not set.
*
* Note, this should only be used during the build process.
*
* If you need to check the environment at runtime, use `process.env.NODE_ENV` to
* ensure that module tree-shaking works correctly.
*
*/
export const NodeEnvironment = process.env.NODE_ENV || "development";

View File

@ -1,6 +1,7 @@
/// <reference types="./types/global.js" /> /// <reference types="./types/global.js" />
export * from "./paths.js"; export * from "./paths.js";
export * from "./environment.js"; export * from "./constants.js";
export * from "./build.js";
export * from "./version.js"; export * from "./version.js";
export * from "./scripting.js"; export * from "./scripting.js";

View File

@ -47,16 +47,7 @@ class SimpleFlowExecutor {
return `${ak().api.base}api/v3/flows/executor/${this.flowSlug}/?query=${encodeURIComponent(window.location.search.substring(1))}`; return `${ak().api.base}api/v3/flows/executor/${this.flowSlug}/?query=${encodeURIComponent(window.location.search.substring(1))}`;
} }
loading() {
this.container.innerHTML = `<div class="d-flex justify-content-center">
<div class="spinner-border spinner-border-md" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>`;
}
start() { start() {
this.loading();
$.ajax({ $.ajax({
type: "GET", type: "GET",
url: this.apiURL, url: this.apiURL,
@ -210,9 +201,6 @@ class PasswordStage extends Stage<PasswordChallenge> {
<form id="password-form"> <form id="password-form">
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt=""> <img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1> <h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
<div class="form-label-group my-3">
<input type="text" readonly class="form-control-plaintext" value="Welcome, ${this.challenge?.pendingUser}.">
</div>
<div class="form-label-group my-3 has-validation"> <div class="form-label-group my-3 has-validation">
<input type="password" autofocus class="form-control ${this.error("password").length > 0 ? IS_INVALID : ""}" name="password" placeholder="Password"> <input type="password" autofocus class="form-control ${this.error("password").length > 0 ? IS_INVALID : ""}" name="password" placeholder="Password">
${this.renderInputError("password")} ${this.renderInputError("password")}

View File

@ -1,32 +1,17 @@
import { spawnSync } from "child_process";
import fs from "fs";
import path from "path";
import process from "process";
/** /**
* @file Lit Localize build script. * Determines if all the Xliff translation source files are present and if the Typescript source
* * files generated from those sources are up-to-date. If they are not, it runs the locale building
* @remarks * script, intercepting the long spew of "this string is not translated" and replacing it with a
* Determines if all the Xliff translation source files are present and if the Typescript source files generated from those sources are up-to-date.
*
* If they are not, it runs the locale building script,
* intercepting the long spew of "this string is not translated" and replacing it with a
* summary of how many strings are missing with respect to the source locale. * summary of how many strings are missing with respect to the source locale.
*
* @import { ConfigFile } from "@lit/localize-tools/lib/types/config"
*/ */
import { PackageRoot } from "@goauthentik/web/paths";
import { spawnSync } from "node:child_process";
import { readFileSync, statSync } from "node:fs";
import path from "node:path";
/** const localizeRules = JSON.parse(fs.readFileSync("./lit-localize.json", "utf-8"));
* @type {ConfigFile}
*/
const localizeRules = JSON.parse(
readFileSync(path.join(PackageRoot, "lit-localize.json"), "utf-8"),
);
/**
*
* @param {string} loc
* @returns {boolean}
*/
function generatedFileIsUpToDateWithXliffSource(loc) { function generatedFileIsUpToDateWithXliffSource(loc) {
const xliff = path.join("./xliff", `${loc}.xlf`); const xliff = path.join("./xliff", `${loc}.xlf`);
const gened = path.join("./src/locales", `${loc}.ts`); const gened = path.join("./src/locales", `${loc}.ts`);
@ -37,7 +22,7 @@ function generatedFileIsUpToDateWithXliffSource(loc) {
// generates a unique error message and halts the build. // generates a unique error message and halts the build.
try { try {
var xlfStat = statSync(xliff); var xlfStat = fs.statSync(xliff);
} catch (_error) { } catch (_error) {
console.error(`lit-localize expected '${loc}.xlf', but XLF file is not present`); console.error(`lit-localize expected '${loc}.xlf', but XLF file is not present`);
process.exit(1); process.exit(1);
@ -45,7 +30,7 @@ function generatedFileIsUpToDateWithXliffSource(loc) {
// If the generated file doesn't exist, of course it's not up to date. // If the generated file doesn't exist, of course it's not up to date.
try { try {
var genedStat = statSync(gened); var genedStat = fs.statSync(gened);
} catch (_error) { } catch (_error) {
return false; return false;
} }

View File

@ -1,4 +1,3 @@
/// <reference types="../types/esbuild.js" />
/** /**
* @file ESBuild script for building the authentik web UI. * @file ESBuild script for building the authentik web UI.
* *
@ -10,6 +9,7 @@ import {
NodeEnvironment, NodeEnvironment,
readBuildIdentifier, readBuildIdentifier,
resolvePackage, resolvePackage,
serializeEnvironmentVars,
} from "@goauthentik/monorepo"; } from "@goauthentik/monorepo";
import { DistDirectory, DistDirectoryName, EntryPoint, PackageRoot } from "@goauthentik/web/paths"; import { DistDirectory, DistDirectoryName, EntryPoint, PackageRoot } from "@goauthentik/web/paths";
import { deepmerge } from "deepmerge-ts"; import { deepmerge } from "deepmerge-ts";
@ -20,10 +20,15 @@ import * as fs from "node:fs/promises";
import * as path from "node:path"; import * as path from "node:path";
import { mdxPlugin } from "./esbuild/build-mdx-plugin.mjs"; import { mdxPlugin } from "./esbuild/build-mdx-plugin.mjs";
import { createBundleDefinitions } from "./esbuild/environment.mjs";
const logPrefix = "[Build]"; const logPrefix = "[Build]";
const definitions = serializeEnvironmentVars({
NODE_ENV: NodeEnvironment,
CWD: process.cwd(),
AK_API_BASE_PATH: process.env.AK_API_BASE_PATH,
});
const patternflyPath = resolvePackage("@patternfly/patternfly"); const patternflyPath = resolvePackage("@patternfly/patternfly");
/** /**
@ -81,7 +86,7 @@ const BASE_ESBUILD_OPTIONS = {
root: MonoRepoRoot, root: MonoRepoRoot,
}), }),
], ],
define: createBundleDefinitions(), define: definitions,
format: "esm", format: "esm",
logOverride: { logOverride: {
/** /**

View File

@ -1,29 +0,0 @@
/**
* @file ESBuild environment utilities.
*/
import { AuthentikVersion, NodeEnvironment, serializeEnvironmentVars } from "@goauthentik/monorepo";
/**
* Creates a mapping of environment variables to their respective runtime constants.
*/
export function createBundleDefinitions() {
const SerializedNodeEnvironment = /** @type {`"development"` | `"production"`} */ (
JSON.stringify(NodeEnvironment)
);
/**
* @satisfies {Record<ESBuildImportEnvKey, string>}
*/
const envRecord = {
AK_VERSION: AuthentikVersion,
AK_API_BASE_PATH: process.env.AK_API_BASE_PATH ?? "",
};
return {
...serializeEnvironmentVars(envRecord),
// We need to explicitly set this for NPM packages that use `process`
// to determine their environment.
"process.env.NODE_ENV": SerializedNodeEnvironment,
"import.meta.env.NODE_ENV": SerializedNodeEnvironment,
};
}

View File

@ -35,11 +35,6 @@ const __dirname = fileURLToPath(new URL(".", import.meta.url));
const projectRoot = path.join(__dirname, ".."); const projectRoot = path.join(__dirname, "..");
process.chdir(projectRoot); process.chdir(projectRoot);
/**
*
* @param {string[]} flags
* @returns
*/
const hasFlag = (flags) => process.argv.length > 1 && flags.includes(process.argv[2]); const hasFlag = (flags) => process.argv.length > 1 && flags.includes(process.argv[2]);
const [configFile, files] = hasFlag(["-n", "--nightmare"]) const [configFile, files] = hasFlag(["-n", "--nightmare"])

View File

@ -0,0 +1,33 @@
#!/usr/bin/env bash
TARGET="./node_modules/@spotlightjs/overlay/dist/index-"[0-9a-f]*.js
if [[ $(grep -L "QX2" "$TARGET" > /dev/null 2> /dev/null) ]]; then
patch --forward -V none --no-backup-if-mismatch -p0 $TARGET <<EOF
TARGET=$(find "./node_modules/@spotlightjs/overlay/dist/" -name "index-[0-9a-f]*.js");
if ! grep -GL 'QX2 = ' "$TARGET" > /dev/null ; then
patch --forward --no-backup-if-mismatch -p0 "$TARGET" <<EOF
>>>>>>> main
--- a/index-5682ce90.js 2024-06-13 16:19:28
+++ b/index-5682ce90.js 2024-06-13 16:20:23
@@ -4958,11 +4958,10 @@
}
);
}
-const q2 = w.lazy(() => import("./main-3257b7fc.js").then((n) => n.m));
+const q2 = w.lazy(() => import("./main-3257b7fc.js").then((n) => n.m)), QX2 = () => {};
function Gp({
data: n,
- onUpdateData: a = () => {
- },
+ onUpdateData: a = QX2,
editingEnabled: s = !1,
clipboardEnabled: o = !1,
displayDataTypes: c = !1,
EOF
else
echo "spotlight overlay.js patch already applied"
fi

View File

@ -1,36 +1,22 @@
/** import { readFileSync } from "fs";
* @file Pseudo-localization script. import path from "path";
*
* @import { ConfigFile } from "@lit/localize-tools/lib/types/config.js"
* @import { Config } from '@lit/localize-tools/lib/types/config.js';
* @import { ProgramMessage } from "@lit/localize-tools/src/messages.js"
* @import { Locale } from "@lit/localize-tools/src/types/locale.js"
*/
import { PackageRoot } from "@goauthentik/web/paths";
import { readFileSync } from "node:fs";
import path from "node:path";
import pseudolocale from "pseudolocale"; import pseudolocale from "pseudolocale";
import { fileURLToPath } from "url";
import { makeFormatter } from "@lit/localize-tools/lib/formatters/index.js"; import { makeFormatter } from "@lit/localize-tools/lib/formatters/index.js";
import { sortProgramMessages } from "@lit/localize-tools/lib/messages.js"; import { sortProgramMessages } from "@lit/localize-tools/lib/messages.js";
import { TransformLitLocalizer } from "@lit/localize-tools/lib/modes/transform.js"; import { TransformLitLocalizer } from "@lit/localize-tools/lib/modes/transform.js";
const pseudoLocale = /** @type {Locale} */ ("pseudo-LOCALE"); const __dirname = fileURLToPath(new URL(".", import.meta.url));
const pseudoLocale = "pseudo-LOCALE";
const targetLocales = [pseudoLocale]; const targetLocales = [pseudoLocale];
const baseConfig = JSON.parse(readFileSync(path.join(__dirname, "../lit-localize.json"), "utf-8"));
/**
* @type {ConfigFile}
*/
const baseConfig = JSON.parse(readFileSync(path.join(PackageRoot, "lit-localize.json"), "utf-8"));
// Need to make some internal specifications to satisfy the transformer. It doesn't actually matter // Need to make some internal specifications to satisfy the transformer. It doesn't actually matter
// which Localizer we use (transformer or runtime), because all of the functionality we care about // which Localizer we use (transformer or runtime), because all of the functionality we care about
// is in their common parent class, but I had to pick one. Everything else here is just pure // is in their common parent class, but I had to pick one. Everything else here is just pure
// exploitation of the lit/localize-tools internals. // exploitation of the lit/localize-tools internals.
/**
* @satisfies {Config}
*/
const config = { const config = {
...baseConfig, ...baseConfig,
baseDir: path.join(__dirname, ".."), baseDir: path.join(__dirname, ".."),
@ -42,11 +28,6 @@ const config = {
resolve: (path) => path, resolve: (path) => path,
}; };
/**
*
* @param {ProgramMessage} message
* @returns
*/
const pseudoMessagify = (message) => ({ const pseudoMessagify = (message) => ({
name: message.name, name: message.name,
contents: message.contents.map((content) => contents: message.contents.map((content) =>
@ -55,7 +36,7 @@ const pseudoMessagify = (message) => ({
}); });
const localizer = new TransformLitLocalizer(config); const localizer = new TransformLitLocalizer(config);
const { messages } = localizer.extractSourceMessages(); const messages = localizer.extractSourceMessages().messages;
const translations = messages.map(pseudoMessagify); const translations = messages.map(pseudoMessagify);
const sorted = sortProgramMessages([...messages]); const sorted = sortProgramMessages([...messages]);
const formatter = makeFormatter(config); const formatter = makeFormatter(config);

View File

@ -1,4 +1,5 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { VERSION } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global"; import { globalAK } from "@goauthentik/common/global";
import { DefaultBrand } from "@goauthentik/common/ui/config"; import { DefaultBrand } from "@goauthentik/common/ui/config";
import "@goauthentik/elements/EmptyState"; import "@goauthentik/elements/EmptyState";
@ -44,7 +45,7 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton))
} }
return [ return [
[msg("Version"), version.versionCurrent], [msg("Version"), version.versionCurrent],
[msg("UI Version"), import.meta.env.AK_VERSION], [msg("UI Version"), VERSION],
[msg("Build"), build], [msg("Build"), build],
[msg("Python version"), status.runtime.pythonVersion], [msg("Python version"), status.runtime.pythonVersion],
[msg("Platform"), status.runtime.platform], [msg("Platform"), status.runtime.platform],

View File

@ -4,13 +4,13 @@ import { ROUTES } from "@goauthentik/admin/Routes";
import { import {
EVENT_API_DRAWER_TOGGLE, EVENT_API_DRAWER_TOGGLE,
EVENT_NOTIFICATION_DRAWER_TOGGLE, EVENT_NOTIFICATION_DRAWER_TOGGLE,
EVENT_SIDEBAR_TOGGLE,
} from "@goauthentik/common/constants"; } from "@goauthentik/common/constants";
import { configureSentry } from "@goauthentik/common/sentry"; import { configureSentry } from "@goauthentik/common/sentry";
import { me } from "@goauthentik/common/users"; import { me } from "@goauthentik/common/users";
import { WebsocketClient } from "@goauthentik/common/ws"; import { WebsocketClient } from "@goauthentik/common/ws";
import { AuthenticatedInterface } from "@goauthentik/elements/Interface"; import { AuthenticatedInterface } from "@goauthentik/elements/Interface";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider.js"; import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider.js";
import { SidebarToggleEventDetail } from "@goauthentik/elements/PageHeader";
import "@goauthentik/elements/ak-locale-context"; import "@goauthentik/elements/ak-locale-context";
import "@goauthentik/elements/banner/EnterpriseStatusBanner"; import "@goauthentik/elements/banner/EnterpriseStatusBanner";
import "@goauthentik/elements/banner/EnterpriseStatusBanner"; import "@goauthentik/elements/banner/EnterpriseStatusBanner";
@ -26,7 +26,7 @@ import "@goauthentik/elements/sidebar/Sidebar";
import "@goauthentik/elements/sidebar/SidebarItem"; import "@goauthentik/elements/sidebar/SidebarItem";
import { CSSResult, TemplateResult, css, html, nothing } from "lit"; import { CSSResult, TemplateResult, css, html, nothing } from "lit";
import { customElement, eventOptions, property, query } from "lit/decorators.js"; import { customElement, property, query, state } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js"; import { classMap } from "lit/directives/class-map.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css";
@ -43,7 +43,7 @@ import {
renderSidebarItems, renderSidebarItems,
} from "./AdminSidebar.js"; } from "./AdminSidebar.js";
if (import.meta.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {
await import("@goauthentik/esbuild-plugin-live-reload/client"); await import("@goauthentik/esbuild-plugin-live-reload/client");
} }
@ -52,33 +52,28 @@ export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) {
//#region Properties //#region Properties
@property({ type: Boolean }) @property({ type: Boolean })
public notificationDrawerOpen = getURLParam("notificationDrawerOpen", false); notificationDrawerOpen = getURLParam("notificationDrawerOpen", false);
@property({ type: Boolean }) @property({ type: Boolean })
public apiDrawerOpen = getURLParam("apiDrawerOpen", false); apiDrawerOpen = getURLParam("apiDrawerOpen", false);
protected readonly ws: WebsocketClient; ws: WebsocketClient;
@property({ @state()
type: Object, user?: SessionUser;
attribute: false,
reflect: false,
})
public user?: SessionUser;
@query("ak-about-modal") @query("ak-about-modal")
public aboutModal?: AboutModal; aboutModal?: AboutModal;
@property({ type: Boolean, reflect: true }) @property({ type: Boolean, reflect: true })
public sidebarOpen: boolean; public sidebarOpen: boolean;
@eventOptions({ passive: true }) #toggleSidebar = () => {
protected sidebarListener(event: CustomEvent<SidebarToggleEventDetail>) { this.sidebarOpen = !this.sidebarOpen;
this.sidebarOpen = !!event.detail.open; };
}
#sidebarMatcher: MediaQueryList; #sidebarMatcher: MediaQueryList;
#sidebarMediaQueryListener = (event: MediaQueryListEvent) => { #sidebarListener = (event: MediaQueryListEvent) => {
this.sidebarOpen = event.matches; this.sidebarOpen = event.matches;
}; };
@ -86,57 +81,59 @@ export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) {
//#region Styles //#region Styles
static styles: CSSResult[] = [ static get styles(): CSSResult[] {
PFBase, return [
PFPage, PFBase,
PFButton, PFPage,
PFDrawer, PFButton,
PFNav, PFDrawer,
css` PFNav,
.pf-c-page__main, css`
.pf-c-drawer__content, .pf-c-page__main,
.pf-c-page__drawer { .pf-c-drawer__content,
z-index: auto !important; .pf-c-page__drawer {
background-color: transparent; z-index: auto !important;
} background-color: transparent;
.display-none {
display: none;
}
.pf-c-page {
background-color: var(--pf-c-page--BackgroundColor) !important;
}
:host([theme="dark"]) {
/* Global page background colour */
.pf-c-page {
--pf-c-page--BackgroundColor: var(--ak-dark-background);
} }
}
ak-page-navbar { .display-none {
grid-area: header; display: none;
} }
.ak-sidebar { .pf-c-page {
grid-area: nav; background-color: var(--pf-c-page--BackgroundColor) !important;
} }
.pf-c-drawer__panel { :host([theme="dark"]) {
z-index: var(--pf-global--ZIndex--xl); /* Global page background colour */
} .pf-c-page {
`, --pf-c-page--BackgroundColor: var(--ak-dark-background);
]; }
}
ak-page-navbar {
grid-area: header;
}
.ak-sidebar {
grid-area: nav;
}
.pf-c-drawer__panel {
z-index: var(--pf-global--ZIndex--xl);
}
`,
];
}
//#endregion //#endregion
//#region Lifecycle //#region Lifecycle
constructor() { constructor() {
configureSentry(true);
super(); super();
this.ws = new WebsocketClient(); this.ws = new WebsocketClient();
this.#sidebarMatcher = window.matchMedia("(min-width: 1200px)"); this.#sidebarMatcher = window.matchMedia("(min-width: 1200px)");
this.sidebarOpen = this.#sidebarMatcher.matches; this.sidebarOpen = this.#sidebarMatcher.matches;
} }
@ -144,6 +141,8 @@ export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) {
public connectedCallback() { public connectedCallback() {
super.connectedCallback(); super.connectedCallback();
window.addEventListener(EVENT_SIDEBAR_TOGGLE, this.#toggleSidebar);
window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, () => { window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, () => {
this.notificationDrawerOpen = !this.notificationDrawerOpen; this.notificationDrawerOpen = !this.notificationDrawerOpen;
updateURLParams({ updateURLParams({
@ -158,17 +157,17 @@ export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) {
}); });
}); });
this.#sidebarMatcher.addEventListener("change", this.#sidebarMediaQueryListener, { this.#sidebarMatcher.addEventListener("change", this.#sidebarListener);
passive: true,
});
} }
public disconnectedCallback(): void { public disconnectedCallback(): void {
super.disconnectedCallback(); super.disconnectedCallback();
this.#sidebarMatcher.removeEventListener("change", this.#sidebarMediaQueryListener); window.removeEventListener(EVENT_SIDEBAR_TOGGLE, this.#toggleSidebar);
this.#sidebarMatcher.removeEventListener("change", this.#sidebarListener);
} }
async firstUpdated(): Promise<void> { async firstUpdated(): Promise<void> {
configureSentry(true);
this.user = await me(); this.user = await me();
const canAccessAdmin = const canAccessAdmin =
@ -198,7 +197,7 @@ export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) {
return html` <ak-locale-context> return html` <ak-locale-context>
<div class="pf-c-page"> <div class="pf-c-page">
<ak-page-navbar ?open=${this.sidebarOpen} @sidebar-toggle=${this.sidebarListener}> <ak-page-navbar>
<ak-version-banner></ak-version-banner> <ak-version-banner></ak-version-banner>
<ak-enterprise-status interface="admin"></ak-enterprise-status> <ak-enterprise-status interface="admin"></ak-enterprise-status>
</ak-page-navbar> </ak-page-navbar>

View File

@ -8,6 +8,7 @@ import "@goauthentik/admin/admin-overview/cards/WorkerStatusCard";
import "@goauthentik/admin/admin-overview/charts/AdminLoginAuthorizeChart"; import "@goauthentik/admin/admin-overview/charts/AdminLoginAuthorizeChart";
import "@goauthentik/admin/admin-overview/charts/OutpostStatusChart"; import "@goauthentik/admin/admin-overview/charts/OutpostStatusChart";
import "@goauthentik/admin/admin-overview/charts/SyncStatusChart"; import "@goauthentik/admin/admin-overview/charts/SyncStatusChart";
import { VERSION } from "@goauthentik/common/constants";
import { me } from "@goauthentik/common/users"; import { me } from "@goauthentik/common/users";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider.js"; import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider.js";
@ -21,6 +22,8 @@ import { msg, str } from "@lit/localize";
import { CSSResult, TemplateResult, css, html, nothing } from "lit"; import { CSSResult, TemplateResult, css, html, nothing } from "lit";
import { customElement, state } from "lit/decorators.js"; import { customElement, state } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js"; import { classMap } from "lit/directives/class-map.js";
import { map } from "lit/directives/map.js";
import { when } from "lit/directives/when.js";
import PFContent from "@patternfly/patternfly/components/Content/content.css"; import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFDivider from "@patternfly/patternfly/components/Divider/divider.css"; import PFDivider from "@patternfly/patternfly/components/Divider/divider.css";
@ -30,17 +33,21 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { SessionUser } from "@goauthentik/api"; import { SessionUser } from "@goauthentik/api";
function createReleaseNotesURL(semver: string): URL { export function versionFamily(): string {
const segments = semver.split("."); const parts = VERSION.split(".");
const versionFamily = segments.slice(0, -1).join("."); parts.pop();
return parts.join(".");
const release = `${versionFamily}#fixed-in-${segments.join("")}`;
return new URL(`/docs/releases/${release}`, "https://goauthentik.io");
} }
const RELEASE = `${VERSION.split(".").slice(0, -1).join(".")}#fixed-in-${VERSION.replaceAll(
".",
"",
)}`;
const AdminOverviewBase = WithLicenseSummary(AKElement); const AdminOverviewBase = WithLicenseSummary(AKElement);
type Renderer = () => TemplateResult | typeof nothing;
@customElement("ak-admin-overview") @customElement("ak-admin-overview")
export class AdminOverviewPage extends AdminOverviewBase { export class AdminOverviewPage extends AdminOverviewBase {
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
@ -76,11 +83,7 @@ export class AdminOverviewPage extends AdminOverviewBase {
[msg("Check the logs"), paramURL("/events/log")], [msg("Check the logs"), paramURL("/events/log")],
[msg("Explore integrations"), "https://goauthentik.io/integrations/", true], [msg("Explore integrations"), "https://goauthentik.io/integrations/", true],
[msg("Manage users"), paramURL("/identity/users")], [msg("Manage users"), paramURL("/identity/users")],
[ [msg("Check the release notes"), `https://goauthentik.io/docs/releases/${RELEASE}`, true],
msg("Check the release notes"),
createReleaseNotesURL(import.meta.env.AK_VERSION).href,
true,
],
]; ];
@state() @state()
@ -190,6 +193,45 @@ export class AdminOverviewPage extends AdminOverviewBase {
</div>` </div>`
: nothing} `; : nothing} `;
} }
renderActions() {
const release = `${versionFamily()}#fixed-in-${VERSION.replaceAll(".", "")}`;
const quickActions: [string, string][] = [
[msg("Create a new application"), paramURL("/core/applications", { createForm: true })],
[msg("Check the logs"), paramURL("/events/log")],
[msg("Explore integrations"), "https://goauthentik.io/integrations/"],
[msg("Manage users"), paramURL("/identity/users")],
[msg("Check the release notes"), `https://goauthentik.io/docs/releases/${release}`],
];
const action = ([label, url]: [string, string]) => {
const isExternal = url.startsWith("https://");
const ex = (truecase: Renderer, falsecase: Renderer) =>
when(isExternal, truecase, falsecase);
const content = html`${label}${ex(
() => html`<i class="fas fa-external-link-alt ak-external-link"></i>`,
() => nothing,
)}`;
return html`<li>
${ex(
() =>
html`<a
href="${url}"
class="pf-u-mb-xl"
rel="noopener noreferrer"
target="_blank"
>${content}</a
>`,
() => html`<a href="${url}" class="pf-u-mb-xl" )>${content}</a>`,
)}
</li>`;
};
return html`${map(quickActions, action)}`;
}
} }
declare global { declare global {

View File

@ -21,7 +21,7 @@ import { type LocalTypeCreate } from "./ProviderChoices.js";
@customElement("ak-application-wizard-provider-choice-step") @customElement("ak-application-wizard-provider-choice-step")
export class ApplicationWizardProviderChoiceStep extends WithLicenseSummary(ApplicationWizardStep) { export class ApplicationWizardProviderChoiceStep extends WithLicenseSummary(ApplicationWizardStep) {
label = msg("Choose a Provider"); label = msg("Choose A Provider");
@state() @state()
failureMessage = ""; failureMessage = "";

View File

@ -45,9 +45,9 @@ const providerListArgs = (page: number, search = "") => ({
}); });
const dualSelectPairMaker = (item: ProviderBase): DualSelectPair => { const dualSelectPairMaker = (item: ProviderBase): DualSelectPair => {
const label = const label = item.assignedBackchannelApplicationName
item.assignedBackchannelApplicationName || item.assignedApplicationName || item.name; ? item.assignedBackchannelApplicationName
: item.assignedApplicationName;
return [ return [
`${item.pk}`, `${item.pk}`,
html`<div class="selection-main">${label}</div> html`<div class="selection-main">${label}</div>

View File

@ -15,7 +15,7 @@ import { DetailedCountry, GeoIPPolicy, PoliciesApi } from "@goauthentik/api";
import { countryCache } from "./CountryCache"; import { countryCache } from "./CountryCache";
function countryToPair(country: DetailedCountry): DualSelectPair { function countryToPair(country: DetailedCountry): DualSelectPair {
return [country.code, country.name, country.name]; return [country.code, country.name];
} }
@customElement("ak-policy-geoip-form") @customElement("ak-policy-geoip-form")
@ -210,16 +210,17 @@ export class GeoIPPolicyForm extends BasePolicyForm<GeoIPPolicy> {
.getCountries() .getCountries()
.then((results) => { .then((results) => {
if (!search) return results; if (!search) return results;
return results.filter((result) => return results.filter((result) =>
result.name result.name
.toLowerCase() .toLowerCase()
.includes(search.toLowerCase()), .includes(search.toLowerCase()),
); );
}) })
.then((results) => ({ .then((results) => {
options: results.map(countryToPair), return {
})); options: results.map(countryToPair),
};
});
}} }}
.selected=${(this.instance?.countriesObj ?? []).map(countryToPair)} .selected=${(this.instance?.countriesObj ?? []).map(countryToPair)}
available-label="${msg("Available Countries")}" available-label="${msg("Available Countries")}"

View File

@ -1,14 +1,18 @@
import { import {
CSRFHeaderName,
CSRFMiddleware, CSRFMiddleware,
EventMiddleware, EventMiddleware,
LoggingMiddleware, LoggingMiddleware,
} from "@goauthentik/common/api/middleware.js"; } from "@goauthentik/common/api/middleware";
import { EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants.js"; import { EVENT_LOCALE_REQUEST, VERSION } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global.js"; import { globalAK } from "@goauthentik/common/global";
import { SentryMiddleware } from "@goauthentik/common/sentry";
import { Config, Configuration, CoreApi, CurrentBrand, RootApi } from "@goauthentik/api"; import { Config, Configuration, CoreApi, CurrentBrand, RootApi } from "@goauthentik/api";
// HACK: Workaround for ESBuild not being able to hoist import statement across entrypoints.
// This can be removed after ESBuild uses a single build context for all entrypoints.
export { CSRFHeaderName };
let globalConfigPromise: Promise<Config> | undefined = Promise.resolve(globalAK().config); let globalConfigPromise: Promise<Config> | undefined = Promise.resolve(globalAK().config);
export function config(): Promise<Config> { export function config(): Promise<Config> {
if (!globalConfigPromise) { if (!globalConfigPromise) {
@ -62,13 +66,21 @@ export function brand(): Promise<CurrentBrand> {
return globalBrandPromise; return globalBrandPromise;
} }
export function getMetaContent(key: string): string {
const metaEl = document.querySelector<HTMLMetaElement>(`meta[name=${key}]`);
if (!metaEl) return "";
return metaEl.content;
}
export const DEFAULT_CONFIG = new Configuration({ export const DEFAULT_CONFIG = new Configuration({
basePath: `${globalAK().api.base}api/v3`, basePath: `${globalAK().api.base}api/v3`,
headers: {
"sentry-trace": getMetaContent("sentry-trace"),
},
middleware: [ middleware: [
new CSRFMiddleware(), new CSRFMiddleware(),
new EventMiddleware(), new EventMiddleware(),
new LoggingMiddleware(globalAK().brand), new LoggingMiddleware(globalAK().brand),
new SentryMiddleware(),
], ],
}); });
@ -79,6 +91,4 @@ export function AndNext(url: string): string {
return `?next=${encodeURIComponent(url)}`; return `?next=${encodeURIComponent(url)}`;
} }
console.debug( console.debug(`authentik(early): version ${VERSION}, apiBase ${DEFAULT_CONFIG.basePath}`);
`authentik(early): version ${import.meta.env.AK_VERSION}, apiBase ${DEFAULT_CONFIG.basePath}`,
);

View File

@ -1,5 +1,5 @@
import { EVENT_REQUEST_POST } from "@goauthentik/common/constants.js"; import { EVENT_REQUEST_POST } from "@goauthentik/common/constants";
import { getCookie } from "@goauthentik/common/utils.js"; import { getCookie } from "@goauthentik/common/utils";
import { import {
CurrentBrand, CurrentBrand,

View File

@ -1,39 +1,17 @@
/**
* @file Global constants used throughout the application.
*
* @todo Much of this content can be moved to a specific file, element, or component.
*/
/// <reference types="../../types/esbuild.js" />
//#region Patternfly
export const SECONDARY_CLASS = "pf-m-secondary"; export const SECONDARY_CLASS = "pf-m-secondary";
export const SUCCESS_CLASS = "pf-m-success"; export const SUCCESS_CLASS = "pf-m-success";
export const ERROR_CLASS = "pf-m-danger"; export const ERROR_CLASS = "pf-m-danger";
export const PROGRESS_CLASS = "pf-m-in-progress"; export const PROGRESS_CLASS = "pf-m-in-progress";
export const CURRENT_CLASS = "pf-m-current"; export const CURRENT_CLASS = "pf-m-current";
export const VERSION = "2025.4.0";
//#endregion
//#region Application
export const TITLE_DEFAULT = "authentik"; export const TITLE_DEFAULT = "authentik";
/**
* The delimiter used to parse the URL for the current route.
*
* @todo Move this to the ak-router.
*/
export const ROUTE_SEPARATOR = ";"; export const ROUTE_SEPARATOR = ";";
//#endregion
//#region Events
export const EVENT_REFRESH = "ak-refresh"; export const EVENT_REFRESH = "ak-refresh";
export const EVENT_NOTIFICATION_DRAWER_TOGGLE = "ak-notification-toggle"; export const EVENT_NOTIFICATION_DRAWER_TOGGLE = "ak-notification-toggle";
export const EVENT_API_DRAWER_TOGGLE = "ak-api-drawer-toggle"; export const EVENT_API_DRAWER_TOGGLE = "ak-api-drawer-toggle";
export const EVENT_FLOW_INSPECTOR_TOGGLE = "ak-flow-inspector-toggle"; export const EVENT_FLOW_INSPECTOR_TOGGLE = "ak-flow-inspector-toggle";
export const EVENT_SIDEBAR_TOGGLE = "ak-sidebar-toggle";
export const EVENT_WS_MESSAGE = "ak-ws-message"; export const EVENT_WS_MESSAGE = "ak-ws-message";
export const EVENT_FLOW_ADVANCE = "ak-flow-advance"; export const EVENT_FLOW_ADVANCE = "ak-flow-advance";
export const EVENT_LOCALE_CHANGE = "ak-locale-change"; export const EVENT_LOCALE_CHANGE = "ak-locale-change";
@ -43,17 +21,7 @@ export const EVENT_MESSAGE = "ak-message";
export const EVENT_THEME_CHANGE = "ak-theme-change"; export const EVENT_THEME_CHANGE = "ak-theme-change";
export const EVENT_REFRESH_ENTERPRISE = "ak-refresh-enterprise"; export const EVENT_REFRESH_ENTERPRISE = "ak-refresh-enterprise";
//#endregion
//#region WebSocket
export const WS_MSG_TYPE_MESSAGE = "message"; export const WS_MSG_TYPE_MESSAGE = "message";
export const WS_MSG_TYPE_REFRESH = "refresh"; export const WS_MSG_TYPE_REFRESH = "refresh";
//#endregion
//#region LocalStorage
export const LOCALSTORAGE_AUTHENTIK_KEY = "authentik-local-settings"; export const LOCALSTORAGE_AUTHENTIK_KEY = "authentik-local-settings";
//#endregion

View File

@ -1,3 +1,4 @@
import { VERSION } from "@goauthentik/common/constants";
import { SentryIgnoredError } from "@goauthentik/common/sentry"; import { SentryIgnoredError } from "@goauthentik/common/sentry";
export interface PlexPinResponse { export interface PlexPinResponse {
@ -18,7 +19,7 @@ export const DEFAULT_HEADERS = {
"Accept": "application/json", "Accept": "application/json",
"Content-Type": "application/json", "Content-Type": "application/json",
"X-Plex-Product": "authentik", "X-Plex-Product": "authentik",
"X-Plex-Version": import.meta.env.AK_VERSION, "X-Plex-Version": VERSION,
"X-Plex-Device-Vendor": "goauthentik.io", "X-Plex-Device-Vendor": "goauthentik.io",
}; };

View File

@ -1,4 +1,5 @@
import { globalAK } from "@goauthentik/common/global"; import { config } from "@goauthentik/common/api/config";
import { VERSION } from "@goauthentik/common/constants";
import { me } from "@goauthentik/common/users"; import { me } from "@goauthentik/common/users";
import { readInterfaceRouteParam } from "@goauthentik/elements/router/utils"; import { readInterfaceRouteParam } from "@goauthentik/elements/router/utils";
import { import {
@ -9,16 +10,8 @@ import {
setTag, setTag,
setUser, setUser,
} from "@sentry/browser"; } from "@sentry/browser";
import { getTraceData } from "@sentry/core";
import * as Spotlight from "@spotlightjs/spotlight";
import { import { CapabilitiesEnum, Config, ResponseError } from "@goauthentik/api";
CapabilitiesEnum,
FetchParams,
Middleware,
RequestContext,
ResponseError,
} from "@goauthentik/api";
/** /**
* A generic error that can be thrown without triggering Sentry's reporting. * A generic error that can be thrown without triggering Sentry's reporting.
@ -28,94 +21,69 @@ export class SentryIgnoredError extends Error {}
export const TAG_SENTRY_COMPONENT = "authentik.component"; export const TAG_SENTRY_COMPONENT = "authentik.component";
export const TAG_SENTRY_CAPABILITIES = "authentik.capabilities"; export const TAG_SENTRY_CAPABILITIES = "authentik.capabilities";
let _sentryConfigured = false; export async function configureSentry(canDoPpi = false): Promise<Config> {
const cfg = await config();
export function configureSentry(canDoPpi = false) { if (cfg.errorReporting.enabled) {
const cfg = globalAK().config; init({
const debug = cfg.capabilities.includes(CapabilitiesEnum.CanDebug); dsn: cfg.errorReporting.sentryDsn,
if (!cfg.errorReporting.enabled && !debug) { ignoreErrors: [
return cfg; /network/gi,
} /fetch/gi,
init({ /module/gi,
dsn: cfg.errorReporting.sentryDsn, // Error on edge on ios,
ignoreErrors: [ // https://stackoverflow.com/questions/69261499/what-is-instantsearchsdkjsbridgeclearhighlight
/network/gi, /instantSearchSDKJSBridgeClearHighlight/gi,
/fetch/gi, // Seems to be an issue in Safari and Firefox
/module/gi, /MutationObserver.observe/gi,
// Error on edge on ios, /NS_ERROR_FAILURE/gi,
// https://stackoverflow.com/questions/69261499/what-is-instantsearchsdkjsbridgeclearhighlight ],
/instantSearchSDKJSBridgeClearHighlight/gi, release: `authentik@${VERSION}`,
// Seems to be an issue in Safari and Firefox
/MutationObserver.observe/gi,
/NS_ERROR_FAILURE/gi,
],
release: `authentik@${import.meta.env.AK_VERSION}`,
integrations: [
browserTracingIntegration({
// https://docs.sentry.io/platforms/javascript/tracing/instrumentation/automatic-instrumentation/#custom-routing
instrumentNavigation: false,
instrumentPageLoad: false,
traceFetch: false,
}),
],
tracePropagationTargets: [window.location.origin],
tracesSampleRate: debug ? 1.0 : cfg.errorReporting.tracesSampleRate,
environment: cfg.errorReporting.environment,
beforeSend: (
event: ErrorEvent,
hint: EventHint,
): ErrorEvent | PromiseLike<ErrorEvent | null> | null => {
if (!hint) {
return event;
}
if (hint.originalException instanceof SentryIgnoredError) {
return null;
}
if (
hint.originalException instanceof ResponseError ||
hint.originalException instanceof DOMException
) {
return null;
}
return event;
},
});
setTag(TAG_SENTRY_CAPABILITIES, cfg.capabilities.join(","));
if (window.location.pathname.includes("if/")) {
setTag(TAG_SENTRY_COMPONENT, `web/${readInterfaceRouteParam()}`);
}
if (debug) {
Spotlight.init({
injectImmediately: true,
integrations: [ integrations: [
Spotlight.sentry({ browserTracingIntegration({
injectIntoSDK: true, shouldCreateSpanForRequest: (url: string) => {
return url.startsWith(window.location.host);
},
}), }),
], ],
tracesSampleRate: cfg.errorReporting.tracesSampleRate,
environment: cfg.errorReporting.environment,
beforeSend: (
event: ErrorEvent,
hint: EventHint,
): ErrorEvent | PromiseLike<ErrorEvent | null> | null => {
if (!hint) {
return event;
}
if (hint.originalException instanceof SentryIgnoredError) {
return null;
}
if (
hint.originalException instanceof ResponseError ||
hint.originalException instanceof DOMException
) {
return null;
}
return event;
},
}); });
console.debug("authentik/config: Enabled Sentry Spotlight"); setTag(TAG_SENTRY_CAPABILITIES, cfg.capabilities.join(","));
} if (window.location.pathname.includes("if/")) {
if (cfg.errorReporting.sendPii && canDoPpi) { setTag(TAG_SENTRY_COMPONENT, `web/${readInterfaceRouteParam()}`);
me().then((user) => { }
setUser({ email: user.user.email }); if (cfg.capabilities.includes(CapabilitiesEnum.CanDebug)) {
console.debug("authentik/config: Sentry with PII enabled."); const Spotlight = await import("@spotlightjs/spotlight");
});
} else { Spotlight.init({ injectImmediately: true });
console.debug("authentik/config: Sentry enabled."); }
} if (cfg.errorReporting.sendPii && canDoPpi) {
_sentryConfigured = true; me().then((user) => {
} setUser({ email: user.user.email });
console.debug("authentik/config: Sentry with PII enabled.");
export class SentryMiddleware implements Middleware { });
pre?(context: RequestContext): Promise<FetchParams | void> { } else {
if (!_sentryConfigured) { console.debug("authentik/config: Sentry enabled.");
return Promise.resolve(context);
} }
const traceData = getTraceData();
// @ts-ignore
context.init.headers["baggage"] = traceData["baggage"];
// @ts-ignore
context.init.headers["sentry-trace"] = traceData["sentry-trace"];
return Promise.resolve(context);
} }
return cfg;
} }

View File

@ -1,12 +1,3 @@
/**
* @file authentik base UI theme.
*/
/* Defined to better identify the base theme when debugging constructed stylesheets. */
.__AK_UI_BASE__ {
--__AK_UI_BASE__: 1;
}
/* #region Global */ /* #region Global */
:root { :root {

View File

@ -1,48 +1,42 @@
/** /*
* @file Atom One Dark syntax highlighting theme.
*
* @see https://github.com/atom/one-dark-syntax
*/
/* Defined to better identify the One Dark theme when debugging constructed stylesheets. */ Atom One Dark by Daniel Gamage
.__HIGHLIGHT_THEME_ONE_DARK__ { Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax
--__HIGHLIGHT_THEME_ONE_DARK__: 1;
}
:root { base: #282c34
--one-dark-base: #282c34; mono-1: #abb2bf
--one-dark-mono-1: #abb2bf; mono-2: #818896
--one-dark-mono-2: #818896; mono-3: #5c6370
--one-dark-mono-3: #5c6370; hue-1: #56b6c2
--one-dark-hue-1: #56b6c2; hue-2: #61aeee
--one-dark-hue-2: #61aeee; hue-3: #c678dd
--one-dark-hue-3: #c678dd; hue-4: #98c379
--one-dark-hue-4: #98c379; hue-5: #e06c75
--one-dark-hue-5: #e06c75; hue-5-2: #be5046
--one-dark-hue-5-2: #be5046; hue-6: #d19a66
--one-dark-hue-6: #d19a66; hue-6-2: #e6c07b
--one-dark-hue-6-2: #e6c07b;
} */
.hljs { .hljs {
color: var(--one-dark-mono-1); color: #abb2bf;
background: var(--one-dark-base); background: #282c34;
} }
pre:has(.hljs) { pre:has(.hljs) {
background: var(--one-dark-base); background: #282c34;
} }
.hljs-comment, .hljs-comment,
.hljs-quote { .hljs-quote {
color: var(--one-dark-mono-3); color: #5c6370;
font-style: italic; font-style: italic;
} }
.hljs-doctag, .hljs-doctag,
.hljs-keyword, .hljs-keyword,
.hljs-formula { .hljs-formula {
color: var(--one-dark-hue-3); color: #c678dd;
} }
.hljs-section, .hljs-section,
@ -50,11 +44,11 @@ pre:has(.hljs) {
.hljs-selector-tag, .hljs-selector-tag,
.hljs-deletion, .hljs-deletion,
.hljs-subst { .hljs-subst {
color: var(--one-dark-hue-5); color: #e06c75;
} }
.hljs-literal { .hljs-literal {
color: var(--one-dark-hue-1); color: #56b6c2;
} }
.hljs-string, .hljs-string,
@ -62,7 +56,7 @@ pre:has(.hljs) {
.hljs-addition, .hljs-addition,
.hljs-attribute, .hljs-attribute,
.hljs-meta .hljs-string { .hljs-meta .hljs-string {
color: var(--one-dark-hue-4); color: #98c379;
} }
.hljs-attr, .hljs-attr,
@ -73,7 +67,7 @@ pre:has(.hljs) {
.hljs-selector-attr, .hljs-selector-attr,
.hljs-selector-pseudo, .hljs-selector-pseudo,
.hljs-number { .hljs-number {
color: var(--one-dark-hue-6); color: #d19a66;
} }
.hljs-symbol, .hljs-symbol,
@ -82,13 +76,13 @@ pre:has(.hljs) {
.hljs-meta, .hljs-meta,
.hljs-selector-id, .hljs-selector-id,
.hljs-title { .hljs-title {
color: var(--one-dark-hue-2); color: #61aeee;
} }
.hljs-built_in, .hljs-built_in,
.hljs-title.class_, .hljs-title.class_,
.hljs-class .hljs-title { .hljs-class .hljs-title {
color: var(--one-dark-hue-6-2); color: #e6c07b;
} }
.hljs-emphasis { .hljs-emphasis {

View File

@ -1,12 +1,3 @@
/**
* @file authentik dark UI theme.
*/
/* Defined to better identify the dark theme when debugging constructed stylesheets. */
.__AK_UI_DARK__ {
--__AK_UI_DARK__: 1;
}
/* #region Global */ /* #region Global */
:root { :root {
@ -14,6 +5,9 @@
--ak-global--Color--100: var(--ak-dark-foreground) !important; --ak-global--Color--100: var(--ak-dark-foreground) !important;
--pf-c-page__main-section--m-light--BackgroundColor: var(--ak-dark-background-darker); --pf-c-page__main-section--m-light--BackgroundColor: var(--ak-dark-background-darker);
--pf-global--BorderColor--100: var(--ak-dark-background-lighter) !important; --pf-global--BorderColor--100: var(--ak-dark-background-lighter) !important;
--ak-mermaid-message-text: var(--ak-dark-foreground) !important;
--ak-mermaid-box-background-color: var(--ak-dark-background-lighter) !important;
--ak-table-stripe-background: var(--pf-global--BackgroundColor--dark-200);
} }
body { body {
@ -262,13 +256,8 @@ input[type="date"]::-webkit-calendar-picker-indicator {
color: var(--ak-dark-background-lighter); color: var(--ak-dark-background-lighter);
} }
.pf-c-button.pf-m-plain { .pf-c-button.pf-m-plain:hover {
--pf-c-button--m-plain--focus--Color: var(--pf-global--Color--200); color: var(--ak-dark-foreground);
--pf-c-button--m-plain--hover--Color: var(--ak-dark-foreground);
&:focus:hover {
color: var(--pf-c-button--m-plain--hover--Color);
}
} }
.pf-c-button.pf-m-control { .pf-c-button.pf-m-control {

View File

@ -1,27 +1,17 @@
/** /**
* @file Stylesheet utilities. * @file Stylesheet utilities.
*/ */
import { CSSResultOrNative, ReactiveElement, adoptStyles as adoptStyleSheetsShim, css } from "lit"; import { CSSResult, CSSResultOrNative, ReactiveElement, css } from "lit";
/** /**
* Element-like objects containing adoptable stylesheets. * Elements containing adoptable stylesheets.
*
* Note that while these all possess the `adoptedStyleSheets` property,
* browser differences and polyfills may make them not actually adoptable.
*
* This type exists to normalize the different ways of accessing the property.
*/ */
export type StyleRoot = export type StyleSheetParent = Pick<DocumentOrShadowRoot, "adoptedStyleSheets">;
| Document
| ShadowRoot
| DocumentFragment
| HTMLElement
| DocumentOrShadowRoot;
/** /**
* Type-predicate to determine if a given object has adoptable stylesheets. * Type-predicate to determine if a given object has adoptable stylesheets.
*/ */
export function isStyleRoot(input: StyleRoot): input is ShadowRoot { export function isAdoptableStyleSheetParent(input: unknown): input is StyleSheetParent {
// Sanity check - Does the input have the right shape? // Sanity check - Does the input have the right shape?
if (!input || typeof input !== "object") return false; if (!input || typeof input !== "object") return false;
@ -35,12 +25,39 @@ export function isStyleRoot(input: StyleRoot): input is ShadowRoot {
// All we care about is that it's shaped like an array. // All we care about is that it's shaped like an array.
if (!("length" in input.adoptedStyleSheets)) return false; if (!("length" in input.adoptedStyleSheets)) return false;
return typeof input.adoptedStyleSheets.length === "number"; if (typeof input.adoptedStyleSheets.length !== "number") return false;
// Finally is the array mutable?
return "push" in input.adoptedStyleSheets;
} }
/** /**
* Create a lazy-loaded `CSSResult` compatible with Lit's * Assert that the given input can adopt stylesheets.
* element lifecycle. */
export function assertAdoptableStyleSheetParent<T>(
input: T,
): asserts input is T & StyleSheetParent {
if (isAdoptableStyleSheetParent(input)) return;
console.debug("Given input missing `adoptedStyleSheets`", input);
throw new TypeError("Assertion failed: `adoptedStyleSheets` missing in given input");
}
export function resolveStyleSheetParent<T extends HTMLElement | DocumentFragment | Document>(
renderRoot: T,
) {
const styleRoot = "ShadyDOM" in window ? document : renderRoot;
assertAdoptableStyleSheetParent(styleRoot);
return styleRoot;
}
export type StyleSheetInit = string | CSSResult | CSSStyleSheet;
/**
* Given a source of CSS, create a `CSSStyleSheet`.
* *
* @throw {@linkcode TypeError} if the input cannot be converted to a `CSSStyleSheet` * @throw {@linkcode TypeError} if the input cannot be converted to a `CSSStyleSheet`
* *
@ -51,12 +68,8 @@ export function isStyleRoot(input: StyleRoot): input is ShadowRoot {
* *
* It works well when Storybook is running in `dev`, but in `build` it fails. * It works well when Storybook is running in `dev`, but in `build` it fails.
* Storied components will have to map their textual CSS imports. * Storied components will have to map their textual CSS imports.
*
* @see {@linkcode createStyleSheetUnsafe} to create a `CSSStyleSheet` from the given input.
*/ */
export function createCSSResult(input: string | CSSModule | CSSResultOrNative): CSSResultOrNative { export function createStyleSheet(input: string): CSSResult {
if (typeof input !== "string") return input;
const inputTemplate = [input] as unknown as TemplateStringsArray; const inputTemplate = [input] as unknown as TemplateStringsArray;
const result = css(inputTemplate, []); const result = css(inputTemplate, []);
@ -65,91 +78,74 @@ export function createCSSResult(input: string | CSSModule | CSSResultOrNative):
} }
/** /**
* Create a `CSSStyleSheet` from the given input, if it is not already a `CSSStyleSheet`. * Given a source of CSS, create a `CSSStyleSheet`.
* *
* @throw {@linkcode TypeError} if the input cannot be converted to a `CSSStyleSheet` * @see {@linkcode createStyleSheet}
*
* @see {@linkcode createCSSResult} for the lazy-loaded `CSSResult` normalization.
*/ */
export function createStyleSheetUnsafe( export function normalizeCSSSource(css: string): CSSStyleSheet;
input: string | CSSModule | CSSResultOrNative, export function normalizeCSSSource(styleSheet: CSSStyleSheet): CSSStyleSheet;
): CSSStyleSheet { export function normalizeCSSSource(cssResult: CSSResult): CSSResult;
const result = typeof input === "string" ? createCSSResult(input) : input; export function normalizeCSSSource(input: StyleSheetInit): CSSResultOrNative;
export function normalizeCSSSource(input: StyleSheetInit): CSSResultOrNative {
if (typeof input === "string") return createStyleSheet(input);
if (result instanceof CSSStyleSheet) return result; return input;
if (result.styleSheet) return result.styleSheet;
const styleSheet = new CSSStyleSheet();
styleSheet.replaceSync(result.cssText);
return styleSheet;
} }
export type StyleSheetsAction =
| Iterable<CSSStyleSheet>
| ((currentStyleSheets: CSSStyleSheet[]) => Iterable<CSSStyleSheet>);
/** /**
* Set the adopted stylesheets of a given style parent. * Create a `CSSStyleSheet` from the given input.
*
* ```ts
* setAdoptedStyleSheets(document.body, (currentStyleSheets) => [
* ...currentStyleSheets,
* myStyleSheet,
* ]);
* ```
*
* @remarks
* Replacing `adoptedStyleSheets` more than once in the same frame may result in
* the `currentStyleSheets` parameter being out of sync with the actual sheets.
*
* A style root's `adoptedStyleSheets` is a proxy object that only updates when
* DOM is repainted. We can't easily cache the previous entries since the style root
* may polyfilled via ShadyDOM.
*
* Short of using {@linkcode requestAnimationFrame} to sequence the adoption,
* and a visibility toggle to avoid a flash of styles between renders,
* we can't reliably cache the previous entries.
*
* In the meantime, we should try to apply all the sheets in a single frame.
*/ */
export function setAdoptedStyleSheets(styleRoot: StyleRoot, styleSheets: StyleSheetsAction): void { export function createStyleSheetUnsafe(input: StyleSheetInit): CSSStyleSheet {
let changed = false; const result = normalizeCSSSource(input);
if (result instanceof CSSStyleSheet) return result;
const currentAdoptedStyleSheets = isStyleRoot(styleRoot) if (!result.styleSheet) {
? [...styleRoot.adoptedStyleSheets] console.debug(
: []; "authentik/common/stylesheets: CSSResult missing styleSheet, returning empty",
{ result, input },
);
const result = throw new TypeError("Expected a CSSStyleSheet");
typeof styleSheets === "function" ? styleSheets(currentAdoptedStyleSheets) : styleSheets;
const nextAdoptedStyleSheets: CSSStyleSheet[] = [];
for (const [idx, styleSheet] of Array.from(result).entries()) {
const previousStyleSheet = currentAdoptedStyleSheets[idx];
changed ||= previousStyleSheet !== styleSheet;
if (nextAdoptedStyleSheets.includes(styleSheet)) continue;
nextAdoptedStyleSheets.push(styleSheet);
} }
changed ||= nextAdoptedStyleSheets.length !== currentAdoptedStyleSheets.length; return result.styleSheet;
if (!changed) return;
if (styleRoot === document) {
document.adoptedStyleSheets = nextAdoptedStyleSheets;
return;
}
adoptStyleSheetsShim(styleRoot as unknown as ShadowRoot, nextAdoptedStyleSheets);
} }
//#region Debugging /**
* Append stylesheet(s) to the given roots.
*
* @see {@linkcode removeStyleSheet} to remove a stylesheet from a given roots.
*/
export function appendStyleSheet(
styleParent: StyleSheetParent,
...insertions: CSSStyleSheet[]
): void {
insertions = Array.isArray(insertions) ? insertions : [insertions];
for (const styleSheetInsertion of insertions) {
if (styleParent.adoptedStyleSheets.includes(styleSheetInsertion)) return;
styleParent.adoptedStyleSheets = [...styleParent.adoptedStyleSheets, styleSheetInsertion];
}
}
/**
* Remove a stylesheet from the given roots, matching by referential equality.
*
* @see {@linkcode appendStyleSheet} to append a stylesheet to a given roots.
*/
export function removeStyleSheet(
styleParent: StyleSheetParent,
...removals: CSSStyleSheet[]
): void {
const nextAdoptedStyleSheets = styleParent.adoptedStyleSheets.filter(
(styleSheet) => !removals.includes(styleSheet),
);
if (nextAdoptedStyleSheets.length === styleParent.adoptedStyleSheets.length) return;
styleParent.adoptedStyleSheets = nextAdoptedStyleSheets;
}
/** /**
* Serialize a stylesheet to a string. * Serialize a stylesheet to a string.
@ -163,8 +159,8 @@ export function serializeStyleSheet(stylesheet: CSSStyleSheet): string {
/** /**
* Inspect the adopted stylesheets of a given style parent, serializing them to strings. * Inspect the adopted stylesheets of a given style parent, serializing them to strings.
*/ */
export function inspectStyleSheets(styleRoot: ShadowRoot): string[] { export function inspectStyleSheets(styleParent: StyleSheetParent): string[] {
return styleRoot.adoptedStyleSheets.map((styleSheet) => serializeStyleSheet(styleSheet)); return styleParent.adoptedStyleSheets.map((styleSheet) => serializeStyleSheet(styleSheet));
} }
interface InspectedStyleSheetEntry { interface InspectedStyleSheetEntry {
@ -178,11 +174,8 @@ interface InspectedStyleSheetEntry {
* Recursively inspect the adopted stylesheets of a given style parent, serializing them to strings. * Recursively inspect the adopted stylesheets of a given style parent, serializing them to strings.
*/ */
export function inspectStyleSheetTree(element: ReactiveElement): InspectedStyleSheetEntry { export function inspectStyleSheetTree(element: ReactiveElement): InspectedStyleSheetEntry {
if (!isStyleRoot(element.renderRoot)) { const styleParent = resolveStyleSheetParent(element.renderRoot);
throw new TypeError("Cannot inspect a render root that doesn't have adoptable stylesheets"); const styles = inspectStyleSheets(styleParent);
}
const styles = inspectStyleSheets(element.renderRoot);
const tagName = element.tagName.toLowerCase(); const tagName = element.tagName.toLowerCase();
const treewalker = document.createTreeWalker(element.renderRoot, NodeFilter.SHOW_ELEMENT, { const treewalker = document.createTreeWalker(element.renderRoot, NodeFilter.SHOW_ELEMENT, {
@ -193,14 +186,12 @@ export function inspectStyleSheetTree(element: ReactiveElement): InspectedStyleS
return NodeFilter.FILTER_SKIP; return NodeFilter.FILTER_SKIP;
}, },
}); });
const children: InspectedStyleSheetEntry[] = []; const children: InspectedStyleSheetEntry[] = [];
let currentNode: Node | null = treewalker.nextNode(); let currentNode: Node | null = treewalker.nextNode();
while (currentNode) { while (currentNode) {
const childElement = currentNode as ReactiveElement; const childElement = currentNode as ReactiveElement;
if (!isStyleRoot(childElement.renderRoot)) { if (!isAdoptableStyleSheetParent(childElement.renderRoot)) {
currentNode = treewalker.nextNode(); currentNode = treewalker.nextNode();
continue; continue;
} }
@ -223,12 +214,10 @@ export function inspectStyleSheetTree(element: ReactiveElement): InspectedStyleS
}; };
} }
if (import.meta.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {
Object.assign(window, { Object.assign(window, {
inspectStyleSheetTree, inspectStyleSheetTree,
serializeStyleSheet, serializeStyleSheet,
inspectStyleSheets, inspectStyleSheets,
}); });
} }
//#endregion

View File

@ -1,47 +1,10 @@
/** /**
* @file Theme utilities. * @file Theme utilities.
*/ */
import { import { UIConfig } from "@goauthentik/common/ui/config";
type StyleRoot,
createStyleSheetUnsafe,
setAdoptedStyleSheets,
} from "@goauthentik/web/common/stylesheets.js";
import { UIConfig } from "@goauthentik/web/common/ui/config.js";
import AKBase from "@goauthentik/web/common/styles/authentik.css";
import AKBaseDark from "@goauthentik/web/common/styles/theme-dark.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { Config, CurrentBrand, UiThemeEnum } from "@goauthentik/api"; import { Config, CurrentBrand, UiThemeEnum } from "@goauthentik/api";
//#region Stylesheet Exports
/**
* A global style sheet for the Patternfly base styles.
*
* @remarks
*
* While a component *may* import its own instance of the PFBase style sheet,
* this instance ensures referential identity.
*/
export const $PFBase = createStyleSheetUnsafe(PFBase);
/**
* A global style sheet for the authentik base styles.
*
* @see {@linkcode $PFBase} for details.
*/
export const $AKBase = createStyleSheetUnsafe(AKBase);
/**
* A global style sheet for the authentik dark theme.
*
* @see {@linkcode $PFBase} for details.
*/
export const $AKBaseDark = createStyleSheetUnsafe(AKBaseDark);
//#endregion
//#region Scheme Types //#region Scheme Types
/** /**
@ -171,21 +134,15 @@ export function resolveUITheme(
* Effect listener invoked when the color scheme changes. * Effect listener invoked when the color scheme changes.
*/ */
export type UIThemeListener = (currentUITheme: ResolvedUITheme) => void; export type UIThemeListener = (currentUITheme: ResolvedUITheme) => void;
/** /**
* Effect destructor invoked when cleanup is required. * Create an effect that runs
*/
export type UIThemeDestructor = () => void;
/**
* Create an effect that runs UI theme changes.
* *
* @returns A cleanup function that removes the effect. * @returns A cleanup function that removes the effect.
*/ */
export function createUIThemeEffect( export function createUIThemeEffect(
effect: UIThemeListener, effect: UIThemeListener,
listenerOptions?: AddEventListenerOptions, listenerOptions?: AddEventListenerOptions,
): UIThemeDestructor { ): () => void {
const colorSchemeTarget = resolveUITheme(); const colorSchemeTarget = resolveUITheme();
const invertedColorSchemeTarget = UIThemeInversion[colorSchemeTarget]; const invertedColorSchemeTarget = UIThemeInversion[colorSchemeTarget];
@ -217,8 +174,6 @@ export function createUIThemeEffect(
mediaQueryList.removeEventListener("change", changeListener); mediaQueryList.removeEventListener("change", changeListener);
}; };
listenerOptions?.signal?.addEventListener("abort", cleanup);
return cleanup; return cleanup;
} }
@ -226,96 +181,16 @@ export function createUIThemeEffect(
//#region Theme Element //#region Theme Element
/**
* Applies the current UI theme to the given style root.
*
* @param styleRoot The style root to apply the theme to.
* @param currentUITheme The current UI theme to apply.
* @param additionalStyleSheets Additional style sheets to apply, in addition to the theme's base sheets.
* @category CSS
*
* @see {@linkcode setAdoptedStyleSheets} for caveats.
*/
export function applyUITheme(
styleRoot: StyleRoot,
currentUITheme: ResolvedUITheme = resolveUITheme(),
...additionalStyleSheets: Array<CSSStyleSheet | undefined | null>
): void {
setAdoptedStyleSheets(styleRoot, (currentStyleSheets) => {
const appendedSheets = additionalStyleSheets.filter(Boolean) as CSSStyleSheet[];
if (currentUITheme === UiThemeEnum.Dark) {
return [...currentStyleSheets, $AKBaseDark, ...appendedSheets];
}
return [
...currentStyleSheets.filter((styleSheet) => styleSheet !== $AKBaseDark),
...appendedSheets,
];
});
}
/**
* Applies the given theme to the document, i.e. the `<html>` element.
*
* @param hint The color scheme hint to use.
*/
export function applyDocumentTheme(hint: CSSColorSchemeValue | UIThemeHint = "auto"): void {
const preferredColorScheme = formatColorScheme(hint);
const applyStyleSheets: UIThemeListener = (currentUITheme) => {
console.debug(`authentik/theme (document): switching to ${currentUITheme} theme`);
setAdoptedStyleSheets(document, (currentStyleSheets) => {
if (currentUITheme === "dark") {
return [...currentStyleSheets, $PFBase, $AKBase, $AKBaseDark];
}
return [
...currentStyleSheets.filter((styleSheet) => styleSheet !== $AKBaseDark),
$PFBase,
$AKBase,
];
});
document.documentElement.dataset.theme = currentUITheme;
};
if (preferredColorScheme === "auto") {
createUIThemeEffect(applyStyleSheets);
return;
}
applyStyleSheets(preferredColorScheme);
}
/** /**
* An element that can be themed. * An element that can be themed.
*/ */
export interface ThemedElement extends HTMLElement { export interface ThemedElement extends HTMLElement {
/** brand?: CurrentBrand;
* The brand information for the current theme. uiConfig?: UIConfig;
*/ config?: Config;
readonly brand?: CurrentBrand;
/**
* The UI configuration for the current theme,
* typically injected through a Lit Mixin.
*
* @see {@linkcode UIConfig} for details.
*/
readonly uiConfig?: UIConfig;
/**
* An authentik configuration initially provided by the server.
*/
readonly config?: Config;
activeTheme: ResolvedUITheme; activeTheme: ResolvedUITheme;
} }
/**
* Returns the root interface element of the page.
*
* @todo Can this be handled with a Lit Mixin?
*/
export function rootInterface<T extends ThemedElement = ThemedElement>(): T | null { export function rootInterface<T extends ThemedElement = ThemedElement>(): T | null {
const element = document.body.querySelector<T>("[data-ak-interface-root]"); const element = document.body.querySelector<T>("[data-ak-interface-root]");

View File

@ -1,5 +1,5 @@
import { me } from "@goauthentik/common/users.js"; import { me } from "@goauthentik/common/users";
import { isUserRoute } from "@goauthentik/elements/router/utils.js"; import { isUserRoute } from "@goauthentik/elements/router/utils";
import { UiThemeEnum, UserSelf } from "@goauthentik/api"; import { UiThemeEnum, UserSelf } from "@goauthentik/api";
import { CurrentBrand } from "@goauthentik/api"; import { CurrentBrand } from "@goauthentik/api";

View File

@ -1,6 +1,6 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config.js"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants.js"; import { EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants";
import { isResponseErrorLike } from "@goauthentik/common/errors/network.js"; import { isResponseErrorLike } from "@goauthentik/common/errors/network";
import { CoreApi, SessionUser } from "@goauthentik/api"; import { CoreApi, SessionUser } from "@goauthentik/api";

View File

@ -1,4 +1,5 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { VERSION } from "@goauthentik/common/constants";
import { PFSize } from "@goauthentik/common/enums.js"; import { PFSize } from "@goauthentik/common/enums.js";
import { import {
EventContext, EventContext,
@ -75,7 +76,7 @@ ${context.message as string}
**Version and Deployment (please complete the following information):** **Version and Deployment (please complete the following information):**
- authentik version: ${import.meta.env.AK_VERSION} - authentik version: ${VERSION}
- Deployment: [e.g. docker-compose, helm] - Deployment: [e.g. docker-compose, helm]
**Additional context** **Additional context**

View File

@ -31,20 +31,19 @@ const container = (testItem: TemplateResult) =>
li { li {
display: block; display: block;
} }
ak-hint {
--ak-hint--Color: var(--pf-global--Color--dark-100);
}
@media (prefers-color-scheme: dark) {
ak-hint {
--ak-hint--Color: var(--pf-global--Color--light-100);
}
}
p { p {
color: black;
margin-top: 1em; margin-top: 1em;
} }
* {
--ak-hint--Color: black !important;
}
ak-hint-title::part(ak-hint-title),
ak-hint-footer::part(ak-hint-footer),
slotted::(*) {
color: black;
}
</style> </style>
${testItem} ${testItem}

View File

@ -2,7 +2,7 @@ import { AKElement } from "@goauthentik/elements/Base";
import { type SlottedTemplateResult, type Spread } from "@goauthentik/elements/types"; import { type SlottedTemplateResult, type Spread } from "@goauthentik/elements/types";
import { spread } from "@open-wc/lit-helpers"; import { spread } from "@open-wc/lit-helpers";
import { css, html, nothing } from "lit"; import { html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js"; import { classMap } from "lit/directives/class-map.js";
@ -64,15 +64,7 @@ export class Alert extends AKElement implements IAlert {
icon = "fa-exclamation-circle"; icon = "fa-exclamation-circle";
static get styles() { static get styles() {
return [ return [PFBase, PFAlert];
PFBase,
PFAlert,
css`
p {
margin: 0;
}
`,
];
} }
get classmap() { get classmap() {

View File

@ -1,24 +1,30 @@
import { globalAK } from "@goauthentik/common/global.js"; import { globalAK } from "@goauthentik/common/global";
import { import {
StyleRoot, StyleSheetInit,
createCSSResult, StyleSheetParent,
appendStyleSheet,
createStyleSheetUnsafe, createStyleSheetUnsafe,
} from "@goauthentik/common/stylesheets.js"; removeStyleSheet,
resolveStyleSheetParent,
} from "@goauthentik/common/stylesheets";
import { import {
$AKBase,
CSSColorSchemeValue, CSSColorSchemeValue,
ResolvedUITheme, ResolvedUITheme,
ThemedElement, UIThemeListener,
applyUITheme,
createUIThemeEffect, createUIThemeEffect,
formatColorScheme, formatColorScheme,
resolveUITheme, resolveUITheme,
} from "@goauthentik/common/theme.js"; } from "@goauthentik/common/theme";
import { type ThemedElement } from "@goauthentik/common/theme";
import { localized } from "@lit/localize"; import { localized } from "@lit/localize";
import { CSSResult, CSSResultGroup, CSSResultOrNative, LitElement } from "lit"; import { CSSResultGroup, CSSResultOrNative, LitElement } from "lit";
import { property } from "lit/decorators.js"; import { property } from "lit/decorators.js";
import AKGlobal from "@goauthentik/common/styles/authentik.css";
import OneDark from "@goauthentik/common/styles/one-dark.css";
import ThemeDark from "@goauthentik/common/styles/theme-dark.css";
import { UiThemeEnum } from "@goauthentik/api"; import { UiThemeEnum } from "@goauthentik/api";
// Re-export the theme helpers // Re-export the theme helpers
@ -26,58 +32,6 @@ export { rootInterface } from "@goauthentik/common/theme";
@localized() @localized()
export class AKElement extends LitElement implements ThemedElement { export class AKElement extends LitElement implements ThemedElement {
//#region Static Properties
public static styles?: Array<CSSResult | CSSModule>;
protected static override finalizeStyles(styles?: CSSResultGroup): CSSResultOrNative[] {
if (!styles) return [$AKBase];
if (!Array.isArray(styles)) return [createCSSResult(styles), $AKBase];
return [
// ---
...(styles.flat() as CSSResultOrNative[]).map(createCSSResult),
$AKBase,
];
}
//#endregion
//#region Lifecycle
constructor() {
super();
const { brand } = globalAK();
this.preferredColorScheme = formatColorScheme(brand.uiTheme);
this.activeTheme = resolveUITheme(brand?.uiTheme);
this.#customCSSStyleSheet = brand?.brandingCustomCss
? createStyleSheetUnsafe(brand.brandingCustomCss)
: null;
}
public override disconnectedCallback(): void {
this.#themeAbortController?.abort();
super.disconnectedCallback();
}
/**
* Returns the node into which the element should render.
*
* @see {LitElement.createRenderRoot} for more information.
*/
protected override createRenderRoot(): HTMLElement | DocumentFragment {
const renderRoot = super.createRenderRoot();
this.styleRoot ??= renderRoot;
return renderRoot;
}
//#endregion
//#region Properties //#region Properties
/** /**
@ -99,54 +53,87 @@ export class AKElement extends LitElement implements ThemedElement {
//#region Private Properties //#region Private Properties
/** readonly #preferredColorScheme: CSSColorSchemeValue;
* The preferred color scheme used to look up the UI theme.
*/
protected readonly preferredColorScheme: CSSColorSchemeValue;
/** #customCSSStyleSheet: CSSStyleSheet | null;
* A custom CSS style sheet to apply to the element. #darkThemeStyleSheet: CSSStyleSheet | null = null;
*/
readonly #customCSSStyleSheet: CSSStyleSheet | null;
/**
* A controller to abort theme updates, such as when the element is disconnected.
*/
#themeAbortController: AbortController | null = null; #themeAbortController: AbortController | null = null;
/**
* The style root to which the theme is applied.
*/
#styleRoot?: StyleRoot;
protected set styleRoot(nextStyleRoot: StyleRoot | undefined) { //#endregion
//#region Lifecycle
protected static finalizeStyles(styles?: CSSResultGroup): CSSResultOrNative[] {
// Ensure all style sheets being passed are really style sheets.
const baseStyles: StyleSheetInit[] = [AKGlobal, OneDark];
if (!styles) return baseStyles.map(createStyleSheetUnsafe);
if (Array.isArray(styles)) {
return [
//---
...(styles as unknown as CSSResultOrNative[]),
...baseStyles,
].flatMap(createStyleSheetUnsafe);
}
return [styles, ...baseStyles].map(createStyleSheetUnsafe);
}
constructor() {
super();
const { brand } = globalAK();
this.#preferredColorScheme = formatColorScheme(brand.uiTheme);
this.activeTheme = resolveUITheme(brand?.uiTheme);
this.#customCSSStyleSheet = brand?.brandingCustomCss
? createStyleSheetUnsafe(brand.brandingCustomCss)
: null;
}
public disconnectedCallback(): void {
super.disconnectedCallback();
this.#themeAbortController?.abort(); this.#themeAbortController?.abort();
}
this.#styleRoot = nextStyleRoot; #styleRoot?: StyleSheetParent;
if (!nextStyleRoot) return; #dispatchTheme: UIThemeListener = (nextUITheme) => {
if (!this.#styleRoot) return;
if (nextUITheme === UiThemeEnum.Dark) {
this.#darkThemeStyleSheet ||= createStyleSheetUnsafe(ThemeDark);
appendStyleSheet(this.#styleRoot, this.#darkThemeStyleSheet);
this.activeTheme = UiThemeEnum.Dark;
} else if (this.#darkThemeStyleSheet) {
removeStyleSheet(this.#styleRoot, this.#darkThemeStyleSheet);
this.#darkThemeStyleSheet = null;
this.activeTheme = UiThemeEnum.Light;
}
};
protected createRenderRoot(): HTMLElement | DocumentFragment {
const renderRoot = super.createRenderRoot();
this.#styleRoot = resolveStyleSheetParent(renderRoot);
if (this.#customCSSStyleSheet) {
console.debug(`authentik/element[${this.tagName.toLowerCase()}]: Adding custom CSS`);
appendStyleSheet(this.#styleRoot, this.#customCSSStyleSheet);
}
this.#themeAbortController = new AbortController(); this.#themeAbortController = new AbortController();
if (this.preferredColorScheme === "dark") { if (this.#preferredColorScheme === "dark") {
applyUITheme(nextStyleRoot, UiThemeEnum.Dark, this.#customCSSStyleSheet); this.#dispatchTheme(UiThemeEnum.Dark);
} else if (this.#preferredColorScheme === "auto") {
this.activeTheme = UiThemeEnum.Dark; createUIThemeEffect(this.#dispatchTheme, {
} else if (this.preferredColorScheme === "auto") { signal: this.#themeAbortController.signal,
createUIThemeEffect( });
(nextUITheme) => {
applyUITheme(nextStyleRoot, nextUITheme, this.#customCSSStyleSheet);
this.activeTheme = nextUITheme;
},
{
signal: this.#themeAbortController.signal,
},
);
} }
}
protected get styleRoot(): StyleRoot | undefined { return renderRoot;
return this.#styleRoot;
} }
//#endregion //#endregion

View File

@ -1,45 +1,34 @@
import { globalAK } from "@goauthentik/common/global.js"; import {
import { ThemedElement, applyDocumentTheme } from "@goauthentik/common/theme.js"; appendStyleSheet,
import { UIConfig } from "@goauthentik/common/ui/config.js"; createStyleSheetUnsafe,
import { AKElement } from "@goauthentik/elements/Base.js"; resolveStyleSheetParent,
import { VersionContextController } from "@goauthentik/elements/Interface/VersionContextController.js"; } from "@goauthentik/common/stylesheets";
import { ThemedElement } from "@goauthentik/common/theme";
import { UIConfig } from "@goauthentik/common/ui/config";
import { AKElement } from "@goauthentik/elements/Base";
import { VersionContextController } from "@goauthentik/elements/Interface/VersionContextController";
import { ModalOrchestrationController } from "@goauthentik/elements/controllers/ModalOrchestrationController.js"; import { ModalOrchestrationController } from "@goauthentik/elements/controllers/ModalOrchestrationController.js";
import { state } from "lit/decorators.js"; import { state } from "lit/decorators.js";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { import type { Config, CurrentBrand, LicenseSummary, Version } from "@goauthentik/api";
type Config,
type CurrentBrand,
type LicenseSummary,
type Version,
} from "@goauthentik/api";
import { BrandContextController } from "./BrandContextController.js"; import { BrandContextController } from "./BrandContextController";
import { ConfigContextController } from "./ConfigContextController.js"; import { ConfigContextController } from "./ConfigContextController";
import { EnterpriseContextController } from "./EnterpriseContextController.js"; import { EnterpriseContextController } from "./EnterpriseContextController";
const configContext = Symbol("configContext"); const configContext = Symbol("configContext");
const modalController = Symbol("modalController"); const modalController = Symbol("modalController");
const versionContext = Symbol("versionContext"); const versionContext = Symbol("versionContext");
export abstract class LightInterface extends AKElement implements ThemedElement { export abstract class Interface extends AKElement implements ThemedElement {
constructor() { protected static readonly PFBaseStyleSheet = createStyleSheetUnsafe(PFBase);
super();
this.dataset.akInterfaceRoot = this.tagName.toLowerCase();
if (!document.documentElement.dataset.theme) { [configContext]: ConfigContextController;
applyDocumentTheme(globalAK().brand.uiTheme);
}
}
}
export abstract class Interface extends LightInterface implements ThemedElement { [modalController]: ModalOrchestrationController;
static styles = [PFBase];
protected [configContext]: ConfigContextController;
protected [modalController]: ModalOrchestrationController;
@state() @state()
public config?: Config; public config?: Config;
@ -49,6 +38,11 @@ export abstract class Interface extends LightInterface implements ThemedElement
constructor() { constructor() {
super(); super();
const styleParent = resolveStyleSheetParent(document);
this.dataset.akInterfaceRoot = this.tagName.toLowerCase();
appendStyleSheet(styleParent, Interface.PFBaseStyleSheet);
this.addController(new BrandContextController(this)); this.addController(new BrandContextController(this));
this[configContext] = new ConfigContextController(this); this[configContext] = new ConfigContextController(this);

View File

@ -1,4 +1,4 @@
import { AuthenticatedInterface, Interface, LightInterface } from "./Interface"; import { AuthenticatedInterface, Interface } from "./Interface";
export { Interface, AuthenticatedInterface, LightInterface }; export { Interface, AuthenticatedInterface };
export default Interface; export default Interface;

View File

@ -1,4 +1,8 @@
import { EVENT_WS_MESSAGE, TITLE_DEFAULT } from "@goauthentik/common/constants"; import {
EVENT_SIDEBAR_TOGGLE,
EVENT_WS_MESSAGE,
TITLE_DEFAULT,
} from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global"; import { globalAK } from "@goauthentik/common/global";
import { UIConfig, UserDisplay, getConfigForUser } from "@goauthentik/common/ui/config"; import { UIConfig, UserDisplay, getConfigForUser } from "@goauthentik/common/ui/config";
import { DefaultBrand } from "@goauthentik/common/ui/config"; import { DefaultBrand } from "@goauthentik/common/ui/config";
@ -25,14 +29,6 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { SessionUser } from "@goauthentik/api"; import { SessionUser } from "@goauthentik/api";
//#region Events
export interface SidebarToggleEventDetail {
open?: boolean;
}
//#endregion
//#region Page Navbar //#region Page Navbar
export interface PageNavbarDetails { export interface PageNavbarDetails {
@ -49,10 +45,7 @@ export interface PageNavbarDetails {
* dispatched by the `ak-page-header` component. * dispatched by the `ak-page-header` component.
*/ */
@customElement("ak-page-navbar") @customElement("ak-page-navbar")
export class AKPageNavbar export class AKPageNavbar extends WithBrandConfig(AKElement) implements PageNavbarDetails {
extends WithBrandConfig(AKElement)
implements PageNavbarDetails, SidebarToggleEventDetail
{
//#region Static Properties //#region Static Properties
private static elementRef: AKPageNavbar | null = null; private static elementRef: AKPageNavbar | null = null;
@ -267,31 +260,29 @@ export class AKPageNavbar
//#region Properties //#region Properties
@state() @property({ type: String })
icon?: string; icon?: string;
@state() @property({ type: Boolean })
iconImage = false; iconImage = false;
@state() @property({ type: String })
header?: string; header?: string;
@state() @property({ type: String })
description?: string; description?: string;
@state() @property({ type: Boolean })
hasIcon = true; hasIcon = true;
@property({ @property({ type: Boolean })
type: Boolean, open = true;
})
public open?: boolean;
@state() @state()
protected session?: SessionUser; session?: SessionUser;
@state() @state()
protected uiConfig!: UIConfig; uiConfig!: UIConfig;
//#endregion //#endregion
@ -314,10 +305,9 @@ export class AKPageNavbar
this.open = !this.open; this.open = !this.open;
this.dispatchEvent( this.dispatchEvent(
new CustomEvent<SidebarToggleEventDetail>("sidebar-toggle", { new CustomEvent(EVENT_SIDEBAR_TOGGLE, {
bubbles: true, bubbles: true,
composed: true, composed: true,
detail: { open: this.open },
}), }),
); );
} }

View File

@ -12,6 +12,7 @@ import type { DualSelectPair } from "./types.js";
* A top-level component for multi-select elements have dynamically generated "selected" * A top-level component for multi-select elements have dynamically generated "selected"
* lists. * lists.
*/ */
@customElement("ak-dual-select-dynamic-selected") @customElement("ak-dual-select-dynamic-selected")
export class AkDualSelectDynamic extends AkDualSelectProvider { export class AkDualSelectDynamic extends AkDualSelectProvider {
/** /**
@ -22,24 +23,20 @@ export class AkDualSelectDynamic extends AkDualSelectProvider {
* @attr * @attr
*/ */
@property({ attribute: false }) @property({ attribute: false })
selector: (_: DualSelectPair[]) => Promise<DualSelectPair[]> = () => Promise.resolve([]); selector: (_: DualSelectPair[]) => Promise<DualSelectPair[]> = async (_) => Promise.resolve([]);
#didFirstUpdate = false; private firstUpdateHasRun = false;
willUpdate(changed: PropertyValues<this>) { willUpdate(changed: PropertyValues<this>) {
super.willUpdate(changed); super.willUpdate(changed);
// On the first update *only*, even before rendering, when the options are handed up, update // On the first update *only*, even before rendering, when the options are handed up, update
// the selected list with the contents derived from the selector. // the selected list with the contents derived from the selector.
if (!this.firstUpdateHasRun && this.options.length > 0) {
if (this.#didFirstUpdate) return; this.firstUpdateHasRun = true;
if (this.options.length === 0) return; this.selector(this.options).then((selected) => {
this.selected = selected;
this.#didFirstUpdate = true; });
}
this.selector(this.options).then((selected) => {
this.selected = selected;
});
} }
render() { render() {

View File

@ -1,16 +1,18 @@
import { AkControlElement } from "@goauthentik/elements/AkControlElement.js"; import { AkControlElement } from "@goauthentik/elements/AkControlElement.js";
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter.js"; import { debounce } from "@goauthentik/elements/utils/debounce";
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { PropertyValues, html } from "lit"; import { PropertyValues, html } from "lit";
import { customElement, property, state } from "lit/decorators.js"; import { customElement, property, state } from "lit/decorators.js";
import { createRef, ref } from "lit/directives/ref.js"; import { createRef, ref } from "lit/directives/ref.js";
import type { Ref } from "lit/directives/ref.js";
import type { Pagination } from "@goauthentik/api"; import type { Pagination } from "@goauthentik/api";
import "./ak-dual-select.js"; import "./ak-dual-select";
import { AkDualSelect } from "./ak-dual-select.js"; import { AkDualSelect } from "./ak-dual-select";
import { type DataProvider, DualSelectEventType, type DualSelectPair } from "./types.js"; import type { DataProvider, DualSelectPair } from "./types";
/** /**
* @element ak-dual-select-provider * @element ak-dual-select-provider
@ -20,19 +22,18 @@ import { type DataProvider, DualSelectEventType, type DualSelectPair } from "./t
* between authentik and the generic ak-dual-select component; aside from knowing that * between authentik and the generic ak-dual-select component; aside from knowing that
* the Pagination object "looks like Django," the interior components don't know anything * the Pagination object "looks like Django," the interior components don't know anything
* about authentik at all and could be dropped into Gravity unchanged.) * about authentik at all and could be dropped into Gravity unchanged.)
*
*/ */
@customElement("ak-dual-select-provider") @customElement("ak-dual-select-provider")
export class AkDualSelectProvider extends CustomListenerElement(AkControlElement) { export class AkDualSelectProvider extends CustomListenerElement(AkControlElement) {
//#region Properties /** A function that takes a page and returns the DualSelectPair[] collection with which to update
* the "Available" pane.
/**
* A function that takes a page and returns the {@linkcode DualSelectPair DualSelectPair[]}
* collection with which to update the "Available" pane.
* *
* @attr * @attr
*/ */
@property({ type: Object }) @property({ type: Object })
public provider!: DataProvider; provider!: DataProvider;
/** /**
* The list of selected items. This is the *complete* list, not paginated, as presented by a * The list of selected items. This is the *complete* list, not paginated, as presented by a
@ -41,7 +42,7 @@ export class AkDualSelectProvider extends CustomListenerElement(AkControlElement
* @attr * @attr
*/ */
@property({ type: Array }) @property({ type: Array })
public selected: DualSelectPair[] = []; selected: DualSelectPair[] = [];
/** /**
* The label for the left ("available") pane * The label for the left ("available") pane
@ -49,7 +50,7 @@ export class AkDualSelectProvider extends CustomListenerElement(AkControlElement
* @attr * @attr
*/ */
@property({ attribute: "available-label" }) @property({ attribute: "available-label" })
public availableLabel = msg("Available options"); availableLabel = msg("Available options");
/** /**
* The label for the right ("selected") pane * The label for the right ("selected") pane
@ -57,7 +58,7 @@ export class AkDualSelectProvider extends CustomListenerElement(AkControlElement
* @attr * @attr
*/ */
@property({ attribute: "selected-label" }) @property({ attribute: "selected-label" })
public selectedLabel = msg("Selected options"); selectedLabel = msg("Selected options");
/** /**
* The debounce for the search as the user is typing in a request * The debounce for the search as the user is typing in a request
@ -65,125 +66,103 @@ export class AkDualSelectProvider extends CustomListenerElement(AkControlElement
* @attr * @attr
*/ */
@property({ attribute: "search-delay", type: Number }) @property({ attribute: "search-delay", type: Number })
public searchDelay = 250; searchDelay = 250;
public get value() {
return this.dualSelector.value!.selected.map(([k, _]) => k);
}
public json() {
return this.value;
}
//#endregion
//#region State
@state() @state()
protected options: DualSelectPair[] = []; options: DualSelectPair[] = [];
#loading = false; protected dualSelector: Ref<AkDualSelect> = createRef();
#didFirstUpdate = false; protected isLoading = false;
#selected: DualSelectPair[] = [];
#previousSearchValue = ""; private doneFirstUpdate = false;
private internalSelected: DualSelectPair[] = [];
protected pagination?: Pagination; protected pagination?: Pagination;
//#endregion constructor() {
super();
//#region Refs setTimeout(() => this.fetch(1), 0);
this.onNav = this.onNav.bind(this);
protected dualSelector = createRef<AkDualSelect>(); this.onChange = this.onChange.bind(this);
this.onSearch = this.onSearch.bind(this);
//#endregion this.addCustomListener("ak-pagination-nav-to", this.onNav);
this.addCustomListener("ak-dual-select-change", this.onChange);
//#region Lifecycle this.addCustomListener("ak-dual-select-search", this.onSearch);
public connectedCallback(): void {
super.connectedCallback();
this.addCustomListener(DualSelectEventType.NavigateTo, this.#navigationListener);
this.addCustomListener(DualSelectEventType.Change, this.#changeListener);
this.addCustomListener(DualSelectEventType.Search, this.#searchListener);
this.#fetch(1);
} }
willUpdate(changedProperties: PropertyValues<this>) { willUpdate(changedProperties: PropertyValues<this>) {
if (changedProperties.has("selected") && !this.#didFirstUpdate) { if (changedProperties.has("selected") && !this.doneFirstUpdate) {
this.#didFirstUpdate = true; this.doneFirstUpdate = true;
this.#selected = this.selected; this.internalSelected = this.selected;
}
if (changedProperties.has("searchDelay")) {
this.doSearch = debounce(
AkDualSelectProvider.prototype.doSearch.bind(this),
this.searchDelay,
);
} }
if (changedProperties.has("provider")) { if (changedProperties.has("provider")) {
this.pagination = undefined; this.pagination = undefined;
this.#previousSearchValue = ""; this.fetch();
this.#fetch();
} }
} }
//#endregion async fetch(page?: number, search = "") {
if (this.isLoading) {
return;
}
this.isLoading = true;
const goto = page ?? this.pagination?.current ?? 1;
const data = await this.provider(goto, search);
this.pagination = data.pagination;
this.options = data.options;
this.isLoading = false;
}
//#region Private Methods onNav(event: Event) {
if (!(event instanceof CustomEvent)) {
throw new Error(`Expecting a CustomEvent for navigation, received ${event} instead`);
}
this.fetch(event.detail);
}
#fetch = async (page?: number, search = this.#previousSearchValue): Promise<void> => { onChange(event: Event) {
if (this.#loading) return; if (!(event instanceof CustomEvent)) {
throw new Error(`Expecting a CustomEvent for change, received ${event} instead`);
}
this.internalSelected = event.detail.value;
this.selected = this.internalSelected;
}
this.#previousSearchValue = search; onSearch(event: Event) {
this.#loading = true; if (!(event instanceof CustomEvent)) {
throw new Error(`Expecting a CustomEvent for change, received ${event} instead`);
}
this.doSearch(event.detail);
}
page ??= this.pagination?.current ?? 1; doSearch(search: string) {
this.pagination = undefined;
this.fetch(undefined, search);
}
return this.provider(page, search) get value() {
.then((data) => { return this.dualSelector.value!.selected.map(([k, _]) => k);
this.pagination = data.pagination; }
this.options = data.options;
})
.catch((error) => {
console.error(error);
})
.finally(() => {
this.#loading = false;
});
};
//#endregion json() {
return this.value;
//#region Event Listeners }
#navigationListener = (event: CustomEvent<number>) => {
this.#fetch(event.detail, this.#previousSearchValue);
};
#changeListener = (event: CustomEvent<{ value: DualSelectPair[] }>) => {
this.#selected = event.detail.value;
this.selected = this.#selected;
};
#searchListener = (event: CustomEvent<string>) => {
this.#doSearch(event.detail);
};
#searchTimeoutID?: ReturnType<typeof setTimeout>;
#doSearch = (search: string) => {
clearTimeout(this.#searchTimeoutID);
setTimeout(() => {
this.pagination = undefined;
this.#fetch(undefined, search);
}, this.searchDelay);
};
//#endregion
render() { render() {
return html`<ak-dual-select return html`<ak-dual-select
${ref(this.dualSelector)} ${ref(this.dualSelector)}
.options=${this.options} .options=${this.options}
.pages=${this.pagination} .pages=${this.pagination}
.selected=${this.#selected} .selected=${this.internalSelected}
available-label=${this.availableLabel} available-label=${this.availableLabel}
selected-label=${this.selectedLabel} selected-label=${this.selectedLabel}
></ak-dual-select>`; ></ak-dual-select>`;

View File

@ -3,7 +3,6 @@ import {
CustomEmitterElement, CustomEmitterElement,
CustomListenerElement, CustomListenerElement,
} from "@goauthentik/elements/utils/eventEmitter"; } from "@goauthentik/elements/utils/eventEmitter";
import { match } from "ts-pattern";
import { msg, str } from "@lit/localize"; import { msg, str } from "@lit/localize";
import { PropertyValues, html, nothing } from "lit"; import { PropertyValues, html, nothing } from "lit";
@ -16,41 +15,34 @@ import { globalVariables, mainStyles } from "./components/styles.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import "./components/ak-dual-select-available-pane.js"; import "./components/ak-dual-select-available-pane";
import { AkDualSelectAvailablePane } from "./components/ak-dual-select-available-pane.js"; import { AkDualSelectAvailablePane } from "./components/ak-dual-select-available-pane";
import "./components/ak-dual-select-controls.js"; import "./components/ak-dual-select-controls";
import "./components/ak-dual-select-selected-pane.js"; import "./components/ak-dual-select-selected-pane";
import { AkDualSelectSelectedPane } from "./components/ak-dual-select-selected-pane.js"; import { AkDualSelectSelectedPane } from "./components/ak-dual-select-selected-pane";
import "./components/ak-pagination.js"; import "./components/ak-pagination";
import "./components/ak-search-bar.js"; import "./components/ak-search-bar";
import { import {
BasePagination, EVENT_ADD_ALL,
DualSelectEventType, EVENT_ADD_ONE,
DualSelectPair, EVENT_ADD_SELECTED,
SearchbarEventDetail, EVENT_DELETE_ALL,
SearchbarEventSource, EVENT_REMOVE_ALL,
} from "./types.js"; EVENT_REMOVE_ONE,
EVENT_REMOVE_SELECTED,
} from "./constants";
import type { BasePagination, DualSelectPair, SearchbarEvent } from "./types";
function localeComparator(a: DualSelectPair, b: DualSelectPair) { function alphaSort([_k1, v1, s1]: DualSelectPair, [_k2, v2, s2]: DualSelectPair) {
const aSortBy = a[2]; const [l, r] = [s1 !== undefined ? s1 : v1, s2 !== undefined ? s2 : v2];
const bSortBy = b[2]; return l < r ? -1 : l > r ? 1 : 0;
return aSortBy.localeCompare(bSortBy);
} }
function keyfinder(key: string) { function mapDualPairs(pairs: DualSelectPair[]) {
return ([k]: DualSelectPair) => k === key; return new Map(pairs.map(([k, v, _]) => [k, v]));
} }
const DelegatedEvents = [ const styles = [PFBase, PFButton, globalVariables, mainStyles];
DualSelectEventType.AddSelected,
DualSelectEventType.RemoveSelected,
DualSelectEventType.AddAll,
DualSelectEventType.RemoveAll,
DualSelectEventType.DeleteAll,
DualSelectEventType.AddOne,
DualSelectEventType.RemoveOne,
] as const satisfies DualSelectEventType[];
/** /**
* @element ak-dual-select * @element ak-dual-select
@ -61,25 +53,24 @@ const DelegatedEvents = [
* *
* @fires ak-dual-select-change - A custom change event with the current `selected` list. * @fires ak-dual-select-change - A custom change event with the current `selected` list.
*/ */
const keyfinder =
(key: string) =>
([k]: DualSelectPair) =>
k === key;
@customElement("ak-dual-select") @customElement("ak-dual-select")
export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKElement)) { export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKElement)) {
static styles = [PFBase, PFButton, globalVariables, mainStyles]; static get styles() {
return styles;
}
//#region Properties /* The list of options to *currently* show. Note that this is not *all* the options, only the
* currently shown list of options from a pagination collection. */
/**
* The list of options to *currently* show.
*
* Note that this is not *all* the options,
* only the currently shown list of options from a pagination collection.
*/
@property({ type: Array }) @property({ type: Array })
options: DualSelectPair[] = []; options: DualSelectPair[] = [];
/** /* The list of options selected. This is the *entire* list and will not be paginated. */
* The list of options selected.
* This is the *entire* list and will not be paginated.
*/
@property({ type: Array }) @property({ type: Array })
selected: DualSelectPair[] = []; selected: DualSelectPair[] = [];
@ -92,133 +83,138 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE
@property({ attribute: "selected-label" }) @property({ attribute: "selected-label" })
selectedLabel = msg("Selected options"); selectedLabel = msg("Selected options");
//#endregion
//#region State
@state() @state()
protected selectedFilter: string = ""; selectedFilter: string = "";
#selectedKeys: Set<string> = new Set();
//#endregion
//#region Refs
availablePane: Ref<AkDualSelectAvailablePane> = createRef(); availablePane: Ref<AkDualSelectAvailablePane> = createRef();
selectedPane: Ref<AkDualSelectSelectedPane> = createRef(); selectedPane: Ref<AkDualSelectSelectedPane> = createRef();
//#endregion selectedKeys: Set<string> = new Set();
//#region Lifecycle
constructor() { constructor() {
super(); super();
this.handleMove = this.handleMove.bind(this);
for (const eventName of DelegatedEvents) { this.handleSearch = this.handleSearch.bind(this);
this.addCustomListener(eventName, this.#moveListener); [
} EVENT_ADD_ALL,
EVENT_ADD_SELECTED,
EVENT_DELETE_ALL,
EVENT_REMOVE_ALL,
EVENT_REMOVE_SELECTED,
EVENT_ADD_ONE,
EVENT_REMOVE_ONE,
].forEach((eventName: string) => {
this.addCustomListener(eventName, (event: Event) => this.handleMove(eventName, event));
});
this.addCustomListener("ak-dual-select-move", () => { this.addCustomListener("ak-dual-select-move", () => {
this.requestUpdate(); this.requestUpdate();
}); });
this.addCustomListener("ak-search", this.handleSearch);
this.addCustomListener("ak-search", this.#searchListener);
} }
willUpdate(changedProperties: PropertyValues<this>) { willUpdate(changedProperties: PropertyValues<this>) {
if (changedProperties.has("selected")) { if (changedProperties.has("selected")) {
this.#selectedKeys = new Set(this.selected.map(([key]) => key)); this.selectedKeys = new Set(this.selected.map(([key, _]) => key));
} }
// Pagination invalidates available moveables. // Pagination invalidates available moveables.
if (changedProperties.has("options") && this.availablePane.value) { if (changedProperties.has("options") && this.availablePane.value) {
this.availablePane.value.clearMove(); this.availablePane.value.clearMove();
} }
} }
//#endregion handleMove(eventName: string, event: Event) {
if (!(event instanceof CustomEvent)) {
throw new Error(`Expected move event here, got ${eventName}`);
}
//#region Event Listeners switch (eventName) {
case EVENT_ADD_SELECTED: {
#moveListener = (event: CustomEvent<string>) => { this.addSelected();
match(event.type) break;
.with(DualSelectEventType.AddSelected, () => this.addSelected()) }
.with(DualSelectEventType.RemoveSelected, () => this.removeSelected()) case EVENT_REMOVE_SELECTED: {
.with(DualSelectEventType.AddAll, () => this.addAllVisible()) this.removeSelected();
.with(DualSelectEventType.RemoveAll, () => this.removeAllVisible()) break;
.with(DualSelectEventType.DeleteAll, () => this.removeAll()) }
.with(DualSelectEventType.AddOne, () => this.addOne(event.detail)) case EVENT_ADD_ALL: {
.with(DualSelectEventType.RemoveOne, () => this.removeOne(event.detail)) this.addAllVisible();
.otherwise(() => { break;
throw new Error(`Expected move event here, got ${event.type}`); }
}); case EVENT_REMOVE_ALL: {
this.removeAllVisible();
this.dispatchCustomEvent(DualSelectEventType.Change, { value: this.value }); break;
}
case EVENT_DELETE_ALL: {
this.removeAll();
break;
}
case EVENT_ADD_ONE: {
this.addOne(event.detail);
break;
}
case EVENT_REMOVE_ONE: {
this.removeOne(event.detail);
break;
}
default:
throw new Error(
`AkDualSelect.handleMove received unknown event type: ${eventName}`,
);
}
this.dispatchCustomEvent("ak-dual-select-change", { value: this.value });
event.stopPropagation(); event.stopPropagation();
}; }
protected addSelected() {
if (this.availablePane.value!.moveable.length === 0) return;
addSelected() {
if (this.availablePane.value!.moveable.length === 0) {
return;
}
this.selected = this.availablePane.value!.moveable.reduce( this.selected = this.availablePane.value!.moveable.reduce(
(acc, key) => { (acc, key) => {
const value = this.options.find(keyfinder(key)); const value = this.options.find(keyfinder(key));
return value && !acc.find(keyfinder(value[0])) ? [...acc, value] : acc; return value && !acc.find(keyfinder(value[0])) ? [...acc, value] : acc;
}, },
[...this.selected], [...this.selected],
); );
// This is where the information gets... lossy. Dammit. // This is where the information gets... lossy. Dammit.
this.availablePane.value!.clearMove(); this.availablePane.value!.clearMove();
} }
protected addOne(key: string) { addOne(key: string) {
const requested = this.options.find(keyfinder(key)); const requested = this.options.find(keyfinder(key));
if (requested && !this.selected.find(keyfinder(requested[0]))) {
if (!requested) return; this.selected = [...this.selected, requested];
if (this.selected.find(keyfinder(requested[0]))) return; }
this.selected = [...this.selected, requested];
} }
// These are the *currently visible* options; the parent node is responsible for paginating and // These are the *currently visible* options; the parent node is responsible for paginating and
// updating the list of currently visible options; // updating the list of currently visible options;
protected addAllVisible() { addAllVisible() {
// Create a new array of all current options and selected, and de-dupe. // Create a new array of all current options and selected, and de-dupe.
const selected = new Map<string, DualSelectPair>([ const selected = mapDualPairs([...this.options, ...this.selected]);
...this.options.map((pair) => [pair[0], pair] as const), this.selected = Array.from(selected.entries());
...this.selected.map((pair) => [pair[0], pair] as const),
]);
this.selected = Array.from(selected.values());
this.availablePane.value!.clearMove(); this.availablePane.value!.clearMove();
} }
protected removeSelected() { removeSelected() {
if (this.selectedPane.value!.moveable.length === 0) return; if (this.selectedPane.value!.moveable.length === 0) {
return;
}
const deselected = new Set(this.selectedPane.value!.moveable); const deselected = new Set(this.selectedPane.value!.moveable);
this.selected = this.selected.filter(([key]) => !deselected.has(key)); this.selected = this.selected.filter(([key]) => !deselected.has(key));
this.selectedPane.value!.clearMove(); this.selectedPane.value!.clearMove();
} }
protected removeOne(key: string) { removeOne(key: string) {
this.selected = this.selected.filter(([k]) => k !== key); this.selected = this.selected.filter(([k]) => k !== key);
} }
protected removeAllVisible() { removeAllVisible() {
// Remove all the items from selected that are in the *currently visible* options list // Remove all the items from selected that are in the *currently visible* options list
const options = new Set(this.options.map(([k]) => k)); const options = new Set(this.options.map(([k, _]) => k));
this.selected = this.selected.filter(([k]) => !options.has(k)); this.selected = this.selected.filter(([k]) => !options.has(k));
this.selectedPane.value!.clearMove(); this.selectedPane.value!.clearMove();
} }
@ -227,25 +223,24 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE
this.selectedPane.value!.clearMove(); this.selectedPane.value!.clearMove();
} }
#searchListener = (event: CustomEvent<SearchbarEventDetail>) => { handleSearch(event: SearchbarEvent) {
const { source, value } = event.detail; switch (event.detail.source) {
case "ak-dual-list-available-search":
match(source) return this.handleAvailableSearch(event.detail.value);
.with(SearchbarEventSource.Available, () => { case "ak-dual-list-selected-search":
this.dispatchCustomEvent(DualSelectEventType.Search, value); return this.handleSelectedSearch(event.detail.value);
}) }
.with(SearchbarEventSource.Selected, () => {
this.selectedFilter = value;
this.selectedPane.value!.clearMove();
})
.exhaustive();
event.stopPropagation(); event.stopPropagation();
}; }
//#endregion handleAvailableSearch(value: string) {
this.dispatchCustomEvent("ak-dual-select-search", value);
}
//#region Public Getters handleSelectedSearch(value: string) {
this.selectedFilter = value;
this.selectedPane.value!.clearMove();
}
get value() { get value() {
return this.selected; return this.selected;
@ -256,7 +251,7 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE
// added. // added.
const allMoved = const allMoved =
this.options.length === this.options.length ===
this.options.filter(([key, _]) => this.#selectedKeys.has(key)).length; this.options.filter(([key, _]) => this.selectedKeys.has(key)).length;
return this.options.length > 0 && !allMoved; return this.options.length > 0 && !allMoved;
} }
@ -264,8 +259,7 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE
get canRemoveAll() { get canRemoveAll() {
// False if no visible option can be found in the selected list // False if no visible option can be found in the selected list
return ( return (
this.options.length > 0 && this.options.length > 0 && !!this.options.find(([key, _]) => this.selectedKeys.has(key))
!!this.options.find(([key, _]) => this.#selectedKeys.has(key))
); );
} }
@ -273,10 +267,6 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE
return (this.pages?.next ?? 0) > 0 || (this.pages?.previous ?? 0) > 0; return (this.pages?.next ?? 0) > 0 || (this.pages?.previous ?? 0) > 0;
} }
//#endregion
//#region Render
render() { render() {
const selected = const selected =
this.selectedFilter === "" this.selectedFilter === ""
@ -292,15 +282,11 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE
const availableCount = this.availablePane.value?.toMove.size ?? 0; const availableCount = this.availablePane.value?.toMove.size ?? 0;
const selectedCount = this.selectedPane.value?.toMove.size ?? 0; const selectedCount = this.selectedPane.value?.toMove.size ?? 0;
const selectedTotal = selected.length; const selectedTotal = selected.length;
const availableStatus = const availableStatus =
availableCount > 0 ? msg(str`${availableCount} item(s) marked to add.`) : "&nbsp;"; availableCount > 0 ? msg(str`${availableCount} item(s) marked to add.`) : "&nbsp;";
const selectedTotalStatus = msg(str`${selectedTotal} item(s) selected.`); const selectedTotalStatus = msg(str`${selectedTotal} item(s) selected.`);
const selectedCountStatus = const selectedCountStatus =
selectedCount > 0 ? " " + msg(str`${selectedCount} item(s) marked to remove.`) : ""; selectedCount > 0 ? " " + msg(str`${selectedCount} item(s) marked to remove.`) : "";
const selectedStatus = `${selectedTotalStatus} ${selectedCountStatus}`; const selectedStatus = `${selectedTotalStatus} ${selectedCountStatus}`;
return html` return html`
@ -324,7 +310,7 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE
<ak-dual-select-available-pane <ak-dual-select-available-pane
${ref(this.availablePane)} ${ref(this.availablePane)}
.options=${this.options} .options=${this.options}
.selected=${this.#selectedKeys} .selected=${this.selectedKeys}
></ak-dual-select-available-pane> ></ak-dual-select-available-pane>
${this.needPagination ${this.needPagination
? html`<ak-pagination .pages=${this.pages}></ak-pagination>` ? html`<ak-pagination .pages=${this.pages}></ak-pagination>`
@ -358,14 +344,12 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE
<ak-dual-select-selected-pane <ak-dual-select-selected-pane
${ref(this.selectedPane)} ${ref(this.selectedPane)}
.selected=${selected.toSorted(localeComparator)} .selected=${selected.toSorted(alphaSort)}
></ak-dual-select-selected-pane> ></ak-dual-select-selected-pane>
</div> </div>
</div> </div>
`; `;
} }
//#endregion
} }
declare global { declare global {

View File

@ -1,24 +1,26 @@
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter"; import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
import { PropertyValues, html, nothing } from "lit"; import { html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js"; import { customElement, property, state } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js"; import { classMap } from "lit/directives/class-map.js";
import { map } from "lit/directives/map.js"; import { map } from "lit/directives/map.js";
import { createRef, ref } from "lit/directives/ref.js";
import { availablePaneStyles, listStyles } from "./styles.css"; import { availablePaneStyles, listStyles } from "./styles.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFDualListSelector from "@patternfly/patternfly/components/DualListSelector/dual-list-selector.css"; import PFDualListSelector from "@patternfly/patternfly/components/DualListSelector/dual-list-selector.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { DualSelectEventType, DualSelectPair } from "../types.js"; import { EVENT_ADD_ONE } from "../constants";
import type { DualSelectPair } from "../types";
const styles = [PFBase, PFButton, PFDualListSelector, listStyles, availablePaneStyles];
const hostAttributes = [ const hostAttributes = [
["aria-labelledby", "dual-list-selector-available-pane-status"], ["aria-labelledby", "dual-list-selector-available-pane-status"],
["aria-multiselectable", "true"], ["aria-multiselectable", "true"],
["role", "listbox"], ["role", "listbox"],
] as const satisfies Array<[string, string]>; ];
/** /**
* @element ak-dual-select-available-panel * @element ak-dual-select-available-panel
@ -35,108 +37,80 @@ const hostAttributes = [
* *
* It is not expected that the `ak-dual-select-available-move-changed` event will be used; instead, * It is not expected that the `ak-dual-select-available-move-changed` event will be used; instead,
* the attribute will be read by the parent when a control is clicked. * the attribute will be read by the parent when a control is clicked.
*
*/ */
@customElement("ak-dual-select-available-pane") @customElement("ak-dual-select-available-pane")
export class AkDualSelectAvailablePane extends CustomEmitterElement<DualSelectEventType>( export class AkDualSelectAvailablePane extends CustomEmitterElement(AKElement) {
AKElement, static get styles() {
) { return styles;
static styles = [PFBase, PFButton, PFDualListSelector, listStyles, availablePaneStyles]; }
//#region Properties
/* The array of key/value pairs this pane is currently showing */ /* The array of key/value pairs this pane is currently showing */
@property({ type: Array }) @property({ type: Array })
readonly options: DualSelectPair[] = []; readonly options: DualSelectPair[] = [];
/** /* A set (set being easy for lookups) of keys with all the pairs selected, so that the ones
* A set (set being easy for lookups) of keys with all the pairs selected, * currently being shown that have already been selected can be marked and their clicks ignored.
* so that the ones currently being shown that have already been selected *
* can be marked and their clicks ignored.
*/ */
@property({ type: Object }) @property({ type: Object })
readonly selected: Set<string> = new Set(); readonly selected: Set<string> = new Set();
//#endregion /* This is the only mutator for this object. It collects the list of objects the user has
* clicked on *in this pane*. It is explicitly marked as "public" to emphasize that the parent
//#region State * orchestrator for the dual-select widget can and will access it to get the list of keys to be
/**
* This is the only mutator for this object.
* It collects the list of objects the user has clicked on *in this pane*.
*
* It is explicitly marked as "public" to emphasize that the parent orchestrator
* for the dual-select widget can and will access it to get the list of keys to be
* moved (removed) if the user so requests. * moved (removed) if the user so requests.
*
*/ */
@state() @state()
public toMove: Set<string> = new Set(); public toMove: Set<string> = new Set();
//#endregion constructor() {
super();
//#region Refs this.onClick = this.onClick.bind(this);
this.onMove = this.onMove.bind(this);
protected listRef = createRef<HTMLDivElement>(); }
//#region Lifecycle
connectedCallback() { connectedCallback() {
super.connectedCallback(); super.connectedCallback();
hostAttributes.forEach(([attr, value]) => {
for (const [attr, value] of hostAttributes) {
if (!this.hasAttribute(attr)) { if (!this.hasAttribute(attr)) {
this.setAttribute(attr, value); this.setAttribute(attr, value);
} }
} });
} }
protected updated(changed: PropertyValues<this>) { clearMove() {
if (changed.has("options")) {
this.listRef.value?.scrollTo(0, 0);
}
}
//#region Public API
public clearMove() {
this.toMove = new Set(); this.toMove = new Set();
} }
get moveable() { onClick(key: string) {
return Array.from(this.toMove.values()); if (this.selected.has(key)) {
} return;
}
//#endregion
//#region Event Listeners
#clickListener(key: string): void {
if (this.selected.has(key)) return;
if (this.toMove.has(key)) { if (this.toMove.has(key)) {
this.toMove.delete(key); this.toMove.delete(key);
} else { } else {
this.toMove.add(key); this.toMove.add(key);
} }
this.dispatchCustomEvent( this.dispatchCustomEvent(
DualSelectEventType.MoveChanged, "ak-dual-select-available-move-changed",
Array.from(this.toMove.values()).sort(), Array.from(this.toMove.values()).sort(),
); );
this.dispatchCustomEvent("ak-dual-select-move");
this.dispatchCustomEvent(DualSelectEventType.Move);
// Necessary because updating a map won't trigger a state change // Necessary because updating a map won't trigger a state change
this.requestUpdate(); this.requestUpdate();
} }
#moveListener(key: string): void { onMove(key: string) {
this.toMove.delete(key); this.toMove.delete(key);
this.dispatchCustomEvent(EVENT_ADD_ONE, key);
this.dispatchCustomEvent(DualSelectEventType.AddOne, key);
this.requestUpdate(); this.requestUpdate();
} }
//#region Render get moveable() {
return Array.from(this.toMove.values());
}
// DO NOT use `Array.map()` instead of Lit's `map()` function. Lit's `map()` is object-aware and // DO NOT use `Array.map()` instead of Lit's `map()` function. Lit's `map()` is object-aware and
// will not re-arrange or reconstruct the list automatically if the actual sources do not // will not re-arrange or reconstruct the list automatically if the actual sources do not
@ -145,18 +119,17 @@ export class AkDualSelectAvailablePane extends CustomEmitterElement<DualSelectEv
render() { render() {
return html` return html`
<div ${ref(this.listRef)} class="pf-c-dual-list-selector__menu"> <div class="pf-c-dual-list-selector__menu">
<ul class="pf-c-dual-list-selector__list"> <ul class="pf-c-dual-list-selector__list">
${map(this.options, ([key, label]) => { ${map(this.options, ([key, label]) => {
const selected = classMap({ const selected = classMap({
"pf-m-selected": this.toMove.has(key), "pf-m-selected": this.toMove.has(key),
}); });
return html` <li return html` <li
class="pf-c-dual-list-selector__list-item" class="pf-c-dual-list-selector__list-item"
aria-selected="false" aria-selected="false"
@click=${() => this.#clickListener(key)} @click=${() => this.onClick(key)}
@dblclick=${() => this.#moveListener(key)} @dblclick=${() => this.onMove(key)}
role="option" role="option"
data-ak-key=${key} data-ak-key=${key}
tabindex="-1" tabindex="-1"
@ -181,8 +154,6 @@ export class AkDualSelectAvailablePane extends CustomEmitterElement<DualSelectEv
</div> </div>
`; `;
} }
//#endregion
} }
export default AkDualSelectAvailablePane; export default AkDualSelectAvailablePane;

View File

@ -8,7 +8,34 @@ import { customElement, property } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { DualSelectEventType } from "../types.js"; import {
EVENT_ADD_ALL,
EVENT_ADD_SELECTED,
EVENT_DELETE_ALL,
EVENT_REMOVE_ALL,
EVENT_REMOVE_SELECTED,
} from "../constants";
const styles = [
PFBase,
PFButton,
css`
:host {
align-self: center;
padding-right: var(--pf-c-dual-list-selector__controls--PaddingRight);
padding-left: var(--pf-c-dual-list-selector__controls--PaddingLeft);
}
.pf-c-dual-list-selector {
max-width: 4rem;
}
.ak-dual-list-selector__controls {
display: grid;
justify-content: center;
align-content: center;
height: 100%;
}
`,
];
/** /**
* @element ak-dual-select-controls * @element ak-dual-select-controls
@ -16,84 +43,64 @@ import { DualSelectEventType } from "../types.js";
* The "control box" for a dual-list multi-select. It's controlled by the parent orchestrator as to * The "control box" for a dual-list multi-select. It's controlled by the parent orchestrator as to
* whether or not any of its controls are enabled. It sends a variety of messages to the parent * whether or not any of its controls are enabled. It sends a variety of messages to the parent
* orchestrator which will then reconcile the "available" and "selected" panes at need. * orchestrator which will then reconcile the "available" and "selected" panes at need.
*
*/ */
@customElement("ak-dual-select-controls")
export class AkDualSelectControls extends CustomEmitterElement<DualSelectEventType>(AKElement) {
static styles = [
PFBase,
PFButton,
css`
:host {
align-self: center;
padding-right: var(--pf-c-dual-list-selector__controls--PaddingRight);
padding-left: var(--pf-c-dual-list-selector__controls--PaddingLeft);
}
.pf-c-dual-list-selector {
max-width: calc(var(--pf-global--spacer-md, 1rem) * 4);
}
.ak-dual-list-selector__controls {
display: grid;
justify-content: center;
align-content: center;
height: 100%;
}
`,
];
/** @customElement("ak-dual-select-controls")
* Set to true if any *visible* elements can be added to the selected list. export class AkDualSelectControls extends CustomEmitterElement(AKElement) {
static get styles() {
return styles;
}
/* Set to true if any *visible* elements can be added to the selected list
*/ */
@property({ attribute: "add-active", type: Boolean }) @property({ attribute: "add-active", type: Boolean })
addActive = false; addActive = false;
/** /* Set to true if any elements can be removed from the selected list (essentially,
* Set to true if any elements can be removed from the selected list (essentially,
* if the selected list is not empty) * if the selected list is not empty)
*/ */
@property({ attribute: "remove-active", type: Boolean }) @property({ attribute: "remove-active", type: Boolean })
removeActive = false; removeActive = false;
/** /* Set to true if *all* the currently visible elements can be moved
* Set to true if *all* the currently visible elements can be moved
* into the selected list (essentially, if any visible elements are * into the selected list (essentially, if any visible elements are
* not currently selected). * not currently selected)
*/ */
@property({ attribute: "add-all-active", type: Boolean }) @property({ attribute: "add-all-active", type: Boolean })
addAllActive = false; addAllActive = false;
/** /* Set to true if *any* of the elements currently visible in the available
* Set to true if *any* of the elements currently visible in the available
* pane are available to be moved to the selected list, enabling that * pane are available to be moved to the selected list, enabling that
* all of those specific elements be moved out of the selected list. * all of those specific elements be moved out of the selected list
*/ */
@property({ attribute: "remove-all-active", type: Boolean }) @property({ attribute: "remove-all-active", type: Boolean })
removeAllActive = false; removeAllActive = false;
/** /* if deleteAll is enabled, set to true to show that there are elements in the
* if deleteAll is enabled, set to true to show that there are elements in the
* selected list that can be deleted. * selected list that can be deleted.
*/ */
@property({ attribute: "delete-all-active", type: Boolean }) @property({ attribute: "delete-all-active", type: Boolean })
enableDeleteAll = false; enableDeleteAll = false;
/** /* Set to true if you want the `...AllActive` buttons made available. */
* Set to true if you want the `...AllActive` buttons made available.
*/
@property({ attribute: "enable-select-all", type: Boolean }) @property({ attribute: "enable-select-all", type: Boolean })
selectAll = false; selectAll = false;
/** /* Set to true if you want the `ClearAllSelected` button made available */
* Set to true if you want the `ClearAllSelected` button made available
*/
@property({ attribute: "enable-delete-all", type: Boolean }) @property({ attribute: "enable-delete-all", type: Boolean })
deleteAll = false; deleteAll = false;
renderButton( constructor() {
label: string, super();
eventType: DualSelectEventType, this.onClick = this.onClick.bind(this);
active: boolean, }
direction: string,
) { onClick(eventName: string) {
this.dispatchCustomEvent(eventName);
}
renderButton(label: string, event: string, active: boolean, direction: string) {
return html` return html`
<div class="pf-c-dual-list-selector__controls-item"> <div class="pf-c-dual-list-selector__controls-item">
<button <button
@ -102,7 +109,7 @@ export class AkDualSelectControls extends CustomEmitterElement<DualSelectEventTy
aria-label=${label} aria-label=${label}
class="pf-c-button pf-m-plain" class="pf-c-button pf-m-plain"
type="button" type="button"
@click=${() => this.dispatchCustomEvent(eventType)} @click=${() => this.onClick(event)}
data-ouia-component-type="AK/Button" data-ouia-component-type="AK/Button"
> >
<i class="fa ${direction}"></i> <i class="fa ${direction}"></i>
@ -116,7 +123,7 @@ export class AkDualSelectControls extends CustomEmitterElement<DualSelectEventTy
<div class="ak-dual-list-selector__controls"> <div class="ak-dual-list-selector__controls">
${this.renderButton( ${this.renderButton(
msg("Add"), msg("Add"),
DualSelectEventType.AddSelected, EVENT_ADD_SELECTED,
this.addActive, this.addActive,
"fa-angle-right", "fa-angle-right",
)} )}
@ -124,13 +131,13 @@ export class AkDualSelectControls extends CustomEmitterElement<DualSelectEventTy
? html` ? html`
${this.renderButton( ${this.renderButton(
msg("Add All Available"), msg("Add All Available"),
DualSelectEventType.AddAll, EVENT_ADD_ALL,
this.addAllActive, this.addAllActive,
"fa-angle-double-right", "fa-angle-double-right",
)} )}
${this.renderButton( ${this.renderButton(
msg("Remove All Available"), msg("Remove All Available"),
DualSelectEventType.RemoveAll, EVENT_REMOVE_ALL,
this.removeAllActive, this.removeAllActive,
"fa-angle-double-left", "fa-angle-double-left",
)} )}
@ -138,14 +145,14 @@ export class AkDualSelectControls extends CustomEmitterElement<DualSelectEventTy
: nothing} : nothing}
${this.renderButton( ${this.renderButton(
msg("Remove"), msg("Remove"),
DualSelectEventType.RemoveSelected, EVENT_REMOVE_SELECTED,
this.removeActive, this.removeActive,
"fa-angle-left", "fa-angle-left",
)} )}
${this.deleteAll ${this.deleteAll
? html`${this.renderButton( ? html`${this.renderButton(
msg("Remove All"), msg("Remove All"),
DualSelectEventType.DeleteAll, EVENT_DELETE_ALL,
this.enableDeleteAll, this.enableDeleteAll,
"fa-times", "fa-times",
)}` )}`

View File

@ -11,13 +11,16 @@ import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFDualListSelector from "@patternfly/patternfly/components/DualListSelector/dual-list-selector.css"; import PFDualListSelector from "@patternfly/patternfly/components/DualListSelector/dual-list-selector.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { DualSelectEventType, DualSelectPair } from "../types"; import { EVENT_REMOVE_ONE } from "../constants";
import type { DualSelectPair } from "../types";
const styles = [PFBase, PFButton, PFDualListSelector, listStyles, selectedPaneStyles];
const hostAttributes = [ const hostAttributes = [
["aria-labelledby", "dual-list-selector-selected-pane-status"], ["aria-labelledby", "dual-list-selector-selected-pane-status"],
["aria-multiselectable", "true"], ["aria-multiselectable", "true"],
["role", "listbox"], ["role", "listbox"],
] as const satisfies Array<[string, string]>; ];
/** /**
* @element ak-dual-select-available-panel * @element ak-dual-select-available-panel
@ -35,86 +38,68 @@ const hostAttributes = [
* *
*/ */
@customElement("ak-dual-select-selected-pane") @customElement("ak-dual-select-selected-pane")
export class AkDualSelectSelectedPane extends CustomEmitterElement<DualSelectEventType>(AKElement) { export class AkDualSelectSelectedPane extends CustomEmitterElement(AKElement) {
static styles = [PFBase, PFButton, PFDualListSelector, listStyles, selectedPaneStyles]; static get styles() {
return styles;
}
//#region Properties /* The array of key/value pairs that are in the selected list. ALL of them. */
/* The array of key/value pairs that are in the selected list. ALL of them. */
@property({ type: Array }) @property({ type: Array })
readonly selected: DualSelectPair[] = []; readonly selected: DualSelectPair[] = [];
//#endregion /*
* This is the only mutator for this object. It collects the list of objects the user has
//#region State * clicked on *in this pane*. It is explicitly marked as "public" to emphasize that the parent
* orchestrator for the dual-select widget can and will access it to get the list of keys to be
/**
* This is the only mutator for this object.
* It collects the list of objects the user has clicked on *in this pane*.
*
* It is explicitly marked as "public" to emphasize that the parent orchestrator
* for the dual-select widget can and will access it to get the list of keys to be
* moved (removed) if the user so requests. * moved (removed) if the user so requests.
*
*/ */
@state() @state()
public toMove: Set<string> = new Set(); public toMove: Set<string> = new Set();
//#endregion constructor() {
super();
this.onClick = this.onClick.bind(this);
this.onMove = this.onMove.bind(this);
}
//#region Lifecycle connectedCallback() {
public connectedCallback() {
super.connectedCallback(); super.connectedCallback();
hostAttributes.forEach(([attr, value]) => {
for (const [attr, value] of hostAttributes) {
if (!this.hasAttribute(attr)) { if (!this.hasAttribute(attr)) {
this.setAttribute(attr, value); this.setAttribute(attr, value);
} }
} });
} }
//#endregion clearMove() {
//#region Public API
public clearMove() {
this.toMove = new Set(); this.toMove = new Set();
} }
public get moveable() { onClick(key: string) {
return Array.from(this.toMove.values());
}
//#endregion
//#region Event Listeners
#clickListener = (key: string): void => {
if (this.toMove.has(key)) { if (this.toMove.has(key)) {
this.toMove.delete(key); this.toMove.delete(key);
} else { } else {
this.toMove.add(key); this.toMove.add(key);
} }
this.dispatchCustomEvent( this.dispatchCustomEvent(
DualSelectEventType.MoveChanged, "ak-dual-select-selected-move-changed",
Array.from(this.toMove.values()).sort(), Array.from(this.toMove.values()).sort(),
); );
this.dispatchCustomEvent("ak-dual-select-move"); this.dispatchCustomEvent("ak-dual-select-move");
// Necessary because updating a map won't trigger a state change // Necessary because updating a map won't trigger a state change
this.requestUpdate(); this.requestUpdate();
}; }
#moveListener = (key: string): void => { onMove(key: string) {
this.toMove.delete(key); this.toMove.delete(key);
this.dispatchCustomEvent(EVENT_REMOVE_ONE, key);
this.dispatchCustomEvent(DualSelectEventType.RemoveOne, key);
this.requestUpdate(); this.requestUpdate();
}; }
//#endregion get moveable() {
return Array.from(this.toMove.values());
//#region Render }
render() { render() {
return html` return html`
@ -128,8 +113,8 @@ export class AkDualSelectSelectedPane extends CustomEmitterElement<DualSelectEve
class="pf-c-dual-list-selector__list-item" class="pf-c-dual-list-selector__list-item"
aria-selected="false" aria-selected="false"
id="dual-list-selector-basic-selected-pane-list-option-0" id="dual-list-selector-basic-selected-pane-list-option-0"
@click=${() => this.#clickListener(key)} @click=${() => this.onClick(key)}
@dblclick=${() => this.#moveListener(key)} @dblclick=${() => this.onMove(key)}
role="option" role="option"
data-ak-key=${key} data-ak-key=${key}
tabindex="-1" tabindex="-1"
@ -149,8 +134,6 @@ export class AkDualSelectSelectedPane extends CustomEmitterElement<DualSelectEve
</div> </div>
`; `;
} }
//#endregion
} }
export default AkDualSelectSelectedPane; export default AkDualSelectSelectedPane;

View File

@ -9,77 +9,85 @@ import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFPagination from "@patternfly/patternfly/components/Pagination/pagination.css"; import PFPagination from "@patternfly/patternfly/components/Pagination/pagination.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { BasePagination, DualSelectEventType } from "../types.js"; import type { BasePagination } from "../types";
const styles = [
PFBase,
PFButton,
PFPagination,
css`
:host([theme="dark"]) .pf-c-pagination__nav-control .pf-c-button {
color: var(--pf-c-button--m-plain--disabled--Color);
--pf-c-button--disabled--Color: var(--pf-c-button--m-plain--Color);
}
:host([theme="dark"]) .pf-c-pagination__nav-control .pf-c-button:disabled {
color: var(--pf-c-button--disabled--Color);
}
`,
];
@customElement("ak-pagination") @customElement("ak-pagination")
export class AkPagination extends CustomEmitterElement<DualSelectEventType>(AKElement) { export class AkPagination extends CustomEmitterElement(AKElement) {
static styles = [ static get styles() {
PFBase, return styles;
PFButton, }
PFPagination,
css`
:host([theme="dark"]) {
.pf-c-pagination__nav-control .pf-c-button {
color: var(--pf-c-button--m-plain--disabled--Color);
--pf-c-button--disabled--Color: var(--pf-c-button--m-plain--Color);
}
.pf-c-pagination__nav-control .pf-c-button:disabled {
color: var(--pf-c-button--disabled--Color);
}
}
`,
];
@property({ attribute: false }) @property({ attribute: false })
pages?: BasePagination; pages?: BasePagination;
#clickListener = (nav: number = 0) => { constructor() {
this.dispatchCustomEvent(DualSelectEventType.NavigateTo, nav); super();
}; this.onClick = this.onClick.bind(this);
}
onClick(nav: number | undefined) {
this.dispatchCustomEvent("ak-pagination-nav-to", nav ?? 0);
}
render() { render() {
const { pages } = this; return this.pages
? html` <div class="pf-c-pagination pf-m-compact pf-m-hidden pf-m-visible-on-md">
if (!pages) return nothing; <div
class="pf-c-pagination pf-m-compact pf-m-compact pf-m-hidden pf-m-visible-on-md"
return html` <div class="pf-c-pagination pf-m-compact pf-m-hidden pf-m-visible-on-md"> >
<div class="pf-c-pagination pf-m-compact pf-m-compact pf-m-hidden pf-m-visible-on-md"> <div class="pf-c-options-menu">
<div class="pf-c-options-menu"> <div class="pf-c-options-menu__toggle pf-m-text pf-m-plain">
<div class="pf-c-options-menu__toggle pf-m-text pf-m-plain"> <span class="pf-c-options-menu__toggle-text">
<span class="pf-c-options-menu__toggle-text"> ${msg(
${msg(str`${pages.startIndex} - ${pages.endIndex} of ${pages.count}`)} str`${this.pages?.startIndex} - ${this.pages?.endIndex} of ${this.pages?.count}`,
</span> )}
</div> </span>
</div> </div>
<nav class="pf-c-pagination__nav" aria-label=${msg("Pagination")}> </div>
<div class="pf-c-pagination__nav-control pf-m-prev"> <nav class="pf-c-pagination__nav" aria-label=${msg("Pagination")}>
<button <div class="pf-c-pagination__nav-control pf-m-prev">
class="pf-c-button pf-m-plain" <button
@click=${() => { class="pf-c-button pf-m-plain"
this.#clickListener(pages.previous); @click=${() => {
}} this.onClick(this.pages?.previous);
?disabled="${(pages.previous ?? 0) < 1}" }}
aria-label="${msg("Go to previous page")}" ?disabled="${(this.pages?.previous ?? 0) < 1}"
> aria-label="${msg("Go to previous page")}"
<i class="fas fa-angle-left" aria-hidden="true"></i> >
</button> <i class="fas fa-angle-left" aria-hidden="true"></i>
</div> </button>
<div class="pf-c-pagination__nav-control pf-m-next"> </div>
<button <div class="pf-c-pagination__nav-control pf-m-next">
class="pf-c-button pf-m-plain" <button
@click=${() => { class="pf-c-button pf-m-plain"
this.#clickListener(pages.next); @click=${() => {
}} this.onClick(this.pages?.next);
?disabled="${(pages.next ?? 0) <= 0}" }}
aria-label="${msg("Go to next page")}" ?disabled="${(this.pages?.next ?? 0) <= 0}"
> aria-label="${msg("Go to next page")}"
<i class="fas fa-angle-right" aria-hidden="true"></i> >
</button> <i class="fas fa-angle-right" aria-hidden="true"></i>
</div> </button>
</nav> </div>
</div> </nav>
</div>`; </div>
</div>`
: nothing;
} }
} }

View File

@ -4,45 +4,47 @@ import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
import { html } from "lit"; import { html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { createRef, ref } from "lit/directives/ref.js"; import { createRef, ref } from "lit/directives/ref.js";
import type { Ref } from "lit/directives/ref.js";
import { globalVariables, searchStyles } from "./search.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import type { SearchbarEventDetail, SearchbarEventSource } from "../types.ts"; import type { SearchbarEvent } from "../types";
import { globalVariables, searchStyles } from "./search.css.js";
const styles = [PFBase, globalVariables, searchStyles];
@customElement("ak-search-bar") @customElement("ak-search-bar")
export class AkSearchbar extends CustomEmitterElement(AKElement) { export class AkSearchbar extends CustomEmitterElement(AKElement) {
static styles = [PFBase, globalVariables, searchStyles]; static get styles() {
return styles;
}
@property({ type: String, reflect: true }) @property({ type: String, reflect: true })
public value = ""; value = "";
/** /**
* If you're using more than one search, this token can help listeners distinguishing between * If you're using more than one search, this token can help listeners distinguishing between
* those searches. Lit's own helpers sometimes erase the source and current targets. * those searches. Lit's own helpers sometimes erase the source and current targets.
*/ */
@property({ type: String }) @property({ type: String })
public name?: SearchbarEventSource; name = "";
protected inputRef = createRef<HTMLInputElement>(); input: Ref<HTMLInputElement> = createRef();
#changeListener = () => { constructor() {
const inputElement = this.inputRef.value; super();
this.onChange = this.onChange.bind(this);
}
if (inputElement) { onChange(_event: Event) {
this.value = inputElement.value; if (this.input.value) {
this.value = this.input.value.value;
} }
this.dispatchCustomEvent<SearchbarEvent>("ak-search", {
if (!this.name) {
console.warn("ak-search-bar: no name provided, event will not be dispatched");
return;
}
this.dispatchCustomEvent<SearchbarEventDetail>("ak-search", {
source: this.name, source: this.name,
value: this.value, value: this.value,
}); });
}; }
render() { render() {
return html` return html`
@ -54,8 +56,8 @@ export class AkSearchbar extends CustomEmitterElement(AKElement) {
><input ><input
type="search" type="search"
class="pf-c-text-input-group__text-input" class="pf-c-text-input-group__text-input"
${ref(this.inputRef)} ${ref(this.input)}
@input=${this.#changeListener} @input=${this.onChange}
value="${this.value}" value="${this.value}"
/></span> /></span>
</div> </div>

View File

@ -0,0 +1,7 @@
export const EVENT_ADD_SELECTED = "ak-dual-select-add";
export const EVENT_REMOVE_SELECTED = "ak-dual-select-remove";
export const EVENT_ADD_ALL = "ak-dual-select-add-all";
export const EVENT_REMOVE_ALL = "ak-dual-select-remove-all";
export const EVENT_DELETE_ALL = "ak-dual-select-remove-everything";
export const EVENT_ADD_ONE = "ak-dual-select-add-one";
export const EVENT_REMOVE_ONE = "ak-dual-select-remove-one";

View File

@ -1,7 +1,7 @@
import { AkDualSelect } from "./ak-dual-select";
import "./ak-dual-select";
import { AkDualSelectProvider } from "./ak-dual-select-provider";
import "./ak-dual-select-provider"; import "./ak-dual-select-provider";
import { AkDualSelectProvider } from "./ak-dual-select-provider.js";
import "./ak-dual-select.js";
import { AkDualSelect } from "./ak-dual-select.js";
export { AkDualSelect, AkDualSelectProvider }; export { AkDualSelect, AkDualSelectProvider };
export default AkDualSelect; export default AkDualSelect;

View File

@ -9,7 +9,7 @@ import { Pagination } from "@goauthentik/api";
import "../ak-dual-select"; import "../ak-dual-select";
import { AkDualSelect } from "../ak-dual-select"; import { AkDualSelect } from "../ak-dual-select";
import { DualSelectEventType, type DualSelectPair } from "../types"; import type { DualSelectPair } from "../types";
const goodForYouRaw = ` const goodForYouRaw = `
Apple, Arrowroot, Artichoke, Arugula, Asparagus, Avocado, Bamboo, Banana, Basil, Beet Root, Apple, Arrowroot, Artichoke, Arugula, Asparagus, Avocado, Bamboo, Banana, Basil, Beet Root,
@ -24,8 +24,7 @@ Rosemary, Rutabaga, Shallot, Soybeans, Spinach, Squash, Strawberries, Sweet pota
Thyme, Tomatillo, Tomato, Turnip, Waterchestnut, Watercress, Watermelon, Yams Thyme, Tomatillo, Tomato, Turnip, Waterchestnut, Watercress, Watermelon, Yams
`; `;
const keyToPair = (key: string): DualSelectPair => [slug(key), key, key]; const keyToPair = (key: string): DualSelectPair => [slug(key), key];
const goodForYou: DualSelectPair[] = goodForYouRaw const goodForYou: DualSelectPair[] = goodForYouRaw
.split("\n") .split("\n")
.join(" ") .join(" ")
@ -84,7 +83,7 @@ export class AkSbFruity extends LitElement {
totalPages: Math.ceil(this.options.length / this.pageLength), totalPages: Math.ceil(this.options.length / this.pageLength),
}; };
this.onNavigation = this.onNavigation.bind(this); this.onNavigation = this.onNavigation.bind(this);
this.addEventListener(DualSelectEventType.NavigateTo, this.onNavigation); this.addEventListener("ak-pagination-nav-to", this.onNavigation);
} }
onNavigation(evt: Event) { onNavigation(evt: Event) {

Some files were not shown because too many files have changed in this diff Show More