enterprise: licensing fixes (#6601)
* enterprise: fix unique index for key, fix field names Signed-off-by: Jens Langhammer <jens@goauthentik.io> * enterprise: update UI to match Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
parent
b93d1cd008
commit
168423a54e
|
@ -35,13 +35,13 @@ class LicenseSerializer(ModelSerializer):
|
||||||
"name",
|
"name",
|
||||||
"key",
|
"key",
|
||||||
"expiry",
|
"expiry",
|
||||||
"users",
|
"internal_users",
|
||||||
"external_users",
|
"external_users",
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
"name": {"read_only": True},
|
"name": {"read_only": True},
|
||||||
"expiry": {"read_only": True},
|
"expiry": {"read_only": True},
|
||||||
"users": {"read_only": True},
|
"internal_users": {"read_only": True},
|
||||||
"external_users": {"read_only": True},
|
"external_users": {"read_only": True},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ class LicenseSerializer(ModelSerializer):
|
||||||
class LicenseSummary(PassiveSerializer):
|
class LicenseSummary(PassiveSerializer):
|
||||||
"""Serializer for license status"""
|
"""Serializer for license status"""
|
||||||
|
|
||||||
users = IntegerField(required=True)
|
internal_users = IntegerField(required=True)
|
||||||
external_users = IntegerField(required=True)
|
external_users = IntegerField(required=True)
|
||||||
valid = BooleanField()
|
valid = BooleanField()
|
||||||
show_admin_warning = BooleanField()
|
show_admin_warning = BooleanField()
|
||||||
|
@ -62,9 +62,9 @@ class LicenseSummary(PassiveSerializer):
|
||||||
class LicenseForecastSerializer(PassiveSerializer):
|
class LicenseForecastSerializer(PassiveSerializer):
|
||||||
"""Serializer for license forecast"""
|
"""Serializer for license forecast"""
|
||||||
|
|
||||||
users = IntegerField(required=True)
|
internal_users = IntegerField(required=True)
|
||||||
external_users = IntegerField(required=True)
|
external_users = IntegerField(required=True)
|
||||||
forecasted_users = IntegerField(required=True)
|
forecasted_internal_users = IntegerField(required=True)
|
||||||
forecasted_external_users = IntegerField(required=True)
|
forecasted_external_users = IntegerField(required=True)
|
||||||
|
|
||||||
|
|
||||||
|
@ -111,7 +111,7 @@ class LicenseViewSet(UsedByMixin, ModelViewSet):
|
||||||
latest_valid = datetime.fromtimestamp(total.exp)
|
latest_valid = datetime.fromtimestamp(total.exp)
|
||||||
response = LicenseSummary(
|
response = LicenseSummary(
|
||||||
data={
|
data={
|
||||||
"users": total.users,
|
"internal_users": total.internal_users,
|
||||||
"external_users": total.external_users,
|
"external_users": total.external_users,
|
||||||
"valid": total.is_valid(),
|
"valid": total.is_valid(),
|
||||||
"show_admin_warning": show_admin_warning,
|
"show_admin_warning": show_admin_warning,
|
||||||
|
@ -135,8 +135,8 @@ class LicenseViewSet(UsedByMixin, ModelViewSet):
|
||||||
def forecast(self, request: Request) -> Response:
|
def forecast(self, request: Request) -> Response:
|
||||||
"""Forecast how many users will be required in a year"""
|
"""Forecast how many users will be required in a year"""
|
||||||
last_month = now() - timedelta(days=30)
|
last_month = now() - timedelta(days=30)
|
||||||
# Forecast for default users
|
# Forecast for internal users
|
||||||
users_in_last_month = User.objects.filter(
|
internal_in_last_month = User.objects.filter(
|
||||||
type=UserTypes.INTERNAL, date_joined__gte=last_month
|
type=UserTypes.INTERNAL, date_joined__gte=last_month
|
||||||
).count()
|
).count()
|
||||||
# Forecast for external users
|
# Forecast for external users
|
||||||
|
@ -144,9 +144,9 @@ class LicenseViewSet(UsedByMixin, ModelViewSet):
|
||||||
forecast_for_months = 12
|
forecast_for_months = 12
|
||||||
response = LicenseForecastSerializer(
|
response = LicenseForecastSerializer(
|
||||||
data={
|
data={
|
||||||
"users": LicenseKey.get_default_user_count(),
|
"internal_users": LicenseKey.get_default_user_count(),
|
||||||
"external_users": LicenseKey.get_external_user_count(),
|
"external_users": LicenseKey.get_external_user_count(),
|
||||||
"forecasted_users": (users_in_last_month * forecast_for_months),
|
"forecasted_internal_users": (internal_in_last_month * forecast_for_months),
|
||||||
"forecasted_external_users": (external_in_last_month * forecast_for_months),
|
"forecasted_external_users": (external_in_last_month * forecast_for_months),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Generated by Django 4.2.4 on 2023-08-23 10:06
|
||||||
|
|
||||||
|
import django.contrib.postgres.indexes
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("authentik_enterprise", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="license",
|
||||||
|
old_name="users",
|
||||||
|
new_name="internal_users",
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="license",
|
||||||
|
name="key",
|
||||||
|
field=models.TextField(),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="license",
|
||||||
|
index=django.contrib.postgres.indexes.HashIndex(
|
||||||
|
fields=["key"], name="authentik_e_key_523e13_hash"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -11,6 +11,7 @@ from uuid import uuid4
|
||||||
from cryptography.exceptions import InvalidSignature
|
from cryptography.exceptions import InvalidSignature
|
||||||
from cryptography.x509 import Certificate, load_der_x509_certificate, load_pem_x509_certificate
|
from cryptography.x509 import Certificate, load_der_x509_certificate, load_pem_x509_certificate
|
||||||
from dacite import from_dict
|
from dacite import from_dict
|
||||||
|
from django.contrib.postgres.indexes import HashIndex
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
|
@ -46,7 +47,7 @@ class LicenseKey:
|
||||||
exp: int
|
exp: int
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
users: int
|
internal_users: int
|
||||||
external_users: int
|
external_users: int
|
||||||
flags: list[LicenseFlags] = field(default_factory=list)
|
flags: list[LicenseFlags] = field(default_factory=list)
|
||||||
|
|
||||||
|
@ -87,7 +88,7 @@ class LicenseKey:
|
||||||
active_licenses = License.objects.filter(expiry__gte=now())
|
active_licenses = License.objects.filter(expiry__gte=now())
|
||||||
total = LicenseKey(get_license_aud(), 0, "Summarized license", 0, 0)
|
total = LicenseKey(get_license_aud(), 0, "Summarized license", 0, 0)
|
||||||
for lic in active_licenses:
|
for lic in active_licenses:
|
||||||
total.users += lic.users
|
total.internal_users += lic.internal_users
|
||||||
total.external_users += lic.external_users
|
total.external_users += lic.external_users
|
||||||
exp_ts = int(mktime(lic.expiry.timetuple()))
|
exp_ts = int(mktime(lic.expiry.timetuple()))
|
||||||
if total.exp == 0:
|
if total.exp == 0:
|
||||||
|
@ -123,7 +124,7 @@ class LicenseKey:
|
||||||
|
|
||||||
Only checks the current count, no historical data is checked"""
|
Only checks the current count, no historical data is checked"""
|
||||||
default_users = self.get_default_user_count()
|
default_users = self.get_default_user_count()
|
||||||
if default_users > self.users:
|
if default_users > self.internal_users:
|
||||||
return False
|
return False
|
||||||
active_users = self.get_external_user_count()
|
active_users = self.get_external_user_count()
|
||||||
if active_users > self.external_users:
|
if active_users > self.external_users:
|
||||||
|
@ -153,11 +154,11 @@ class License(models.Model):
|
||||||
"""An authentik enterprise license"""
|
"""An authentik enterprise license"""
|
||||||
|
|
||||||
license_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
license_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
||||||
key = models.TextField(unique=True)
|
key = models.TextField()
|
||||||
|
|
||||||
name = models.TextField()
|
name = models.TextField()
|
||||||
expiry = models.DateTimeField()
|
expiry = models.DateTimeField()
|
||||||
users = models.BigIntegerField()
|
internal_users = models.BigIntegerField()
|
||||||
external_users = models.BigIntegerField()
|
external_users = models.BigIntegerField()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -165,6 +166,9 @@ class License(models.Model):
|
||||||
"""Get parsed license status"""
|
"""Get parsed license status"""
|
||||||
return LicenseKey.validate(self.key)
|
return LicenseKey.validate(self.key)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
indexes = (HashIndex(fields=("key",)),)
|
||||||
|
|
||||||
|
|
||||||
def usage_expiry():
|
def usage_expiry():
|
||||||
"""Keep license usage records for 3 months"""
|
"""Keep license usage records for 3 months"""
|
||||||
|
|
|
@ -13,6 +13,6 @@ def pre_save_license(sender: type[License], instance: License, **_):
|
||||||
"""Extract data from license jwt and save it into model"""
|
"""Extract data from license jwt and save it into model"""
|
||||||
status = instance.status
|
status = instance.status
|
||||||
instance.name = status.name
|
instance.name = status.name
|
||||||
instance.users = status.users
|
instance.internal_users = status.internal_users
|
||||||
instance.external_users = status.external_users
|
instance.external_users = status.external_users
|
||||||
instance.expiry = datetime.fromtimestamp(status.exp, tz=get_current_timezone())
|
instance.expiry = datetime.fromtimestamp(status.exp, tz=get_current_timezone())
|
||||||
|
|
|
@ -23,7 +23,7 @@ class TestEnterpriseLicense(TestCase):
|
||||||
aud="",
|
aud="",
|
||||||
exp=_exp,
|
exp=_exp,
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
users=100,
|
internal_users=100,
|
||||||
external_users=100,
|
external_users=100,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
@ -32,7 +32,7 @@ class TestEnterpriseLicense(TestCase):
|
||||||
"""Check license verification"""
|
"""Check license verification"""
|
||||||
lic = License.objects.create(key=generate_id())
|
lic = License.objects.create(key=generate_id())
|
||||||
self.assertTrue(lic.status.is_valid())
|
self.assertTrue(lic.status.is_valid())
|
||||||
self.assertEqual(lic.users, 100)
|
self.assertEqual(lic.internal_users, 100)
|
||||||
|
|
||||||
def test_invalid(self):
|
def test_invalid(self):
|
||||||
"""Test invalid license"""
|
"""Test invalid license"""
|
||||||
|
@ -46,7 +46,7 @@ class TestEnterpriseLicense(TestCase):
|
||||||
aud="",
|
aud="",
|
||||||
exp=_exp,
|
exp=_exp,
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
users=100,
|
internal_users=100,
|
||||||
external_users=100,
|
external_users=100,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
@ -58,7 +58,7 @@ class TestEnterpriseLicense(TestCase):
|
||||||
lic2 = License.objects.create(key=generate_id())
|
lic2 = License.objects.create(key=generate_id())
|
||||||
self.assertTrue(lic2.status.is_valid())
|
self.assertTrue(lic2.status.is_valid())
|
||||||
total = LicenseKey.get_total()
|
total = LicenseKey.get_total()
|
||||||
self.assertEqual(total.users, 200)
|
self.assertEqual(total.internal_users, 200)
|
||||||
self.assertEqual(total.external_users, 200)
|
self.assertEqual(total.external_users, 200)
|
||||||
self.assertEqual(total.exp, _exp)
|
self.assertEqual(total.exp, _exp)
|
||||||
self.assertTrue(total.is_valid())
|
self.assertTrue(total.is_valid())
|
||||||
|
|
|
@ -8,7 +8,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2023-08-17 17:37+0000\n"
|
"POT-Creation-Date: 2023-08-23 10:04+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
@ -98,125 +98,125 @@ msgstr ""
|
||||||
msgid "Users added to this group will be superusers."
|
msgid "Users added to this group will be superusers."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/core/models.py:162
|
#: authentik/core/models.py:142
|
||||||
msgid "User's display name."
|
msgid "User's display name."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/core/models.py:256 authentik/providers/oauth2/models.py:294
|
#: authentik/core/models.py:268 authentik/providers/oauth2/models.py:294
|
||||||
msgid "User"
|
msgid "User"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/core/models.py:257
|
#: authentik/core/models.py:269
|
||||||
msgid "Users"
|
msgid "Users"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/core/models.py:270
|
#: authentik/core/models.py:282
|
||||||
msgid ""
|
msgid ""
|
||||||
"Flow used for authentication when the associated application is accessed by "
|
"Flow used for authentication when the associated application is accessed by "
|
||||||
"an un-authenticated user."
|
"an un-authenticated user."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/core/models.py:280
|
#: authentik/core/models.py:292
|
||||||
msgid "Flow used when authorizing this provider."
|
msgid "Flow used when authorizing this provider."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/core/models.py:292
|
#: authentik/core/models.py:304
|
||||||
msgid ""
|
msgid ""
|
||||||
"Accessed from applications; optional backchannel providers for protocols "
|
"Accessed from applications; optional backchannel providers for protocols "
|
||||||
"like LDAP and SCIM."
|
"like LDAP and SCIM."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/core/models.py:347
|
#: authentik/core/models.py:359
|
||||||
msgid "Application's display Name."
|
msgid "Application's display Name."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/core/models.py:348
|
#: authentik/core/models.py:360
|
||||||
msgid "Internal application name, used in URLs."
|
msgid "Internal application name, used in URLs."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/core/models.py:360
|
#: authentik/core/models.py:372
|
||||||
msgid "Open launch URL in a new browser tab or window."
|
msgid "Open launch URL in a new browser tab or window."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/core/models.py:424
|
#: authentik/core/models.py:436
|
||||||
msgid "Application"
|
msgid "Application"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/core/models.py:425
|
#: authentik/core/models.py:437
|
||||||
msgid "Applications"
|
msgid "Applications"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/core/models.py:431
|
#: authentik/core/models.py:443
|
||||||
msgid "Use the source-specific identifier"
|
msgid "Use the source-specific identifier"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/core/models.py:433
|
#: authentik/core/models.py:445
|
||||||
msgid ""
|
msgid ""
|
||||||
"Link to a user with identical email address. Can have security implications "
|
"Link to a user with identical email address. Can have security implications "
|
||||||
"when a source doesn't validate email addresses."
|
"when a source doesn't validate email addresses."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/core/models.py:437
|
#: authentik/core/models.py:449
|
||||||
msgid ""
|
msgid ""
|
||||||
"Use the user's email address, but deny enrollment when the email address "
|
"Use the user's email address, but deny enrollment when the email address "
|
||||||
"already exists."
|
"already exists."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/core/models.py:440
|
#: authentik/core/models.py:452
|
||||||
msgid ""
|
msgid ""
|
||||||
"Link to a user with identical username. Can have security implications when "
|
"Link to a user with identical username. Can have security implications when "
|
||||||
"a username is used with another source."
|
"a username is used with another source."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/core/models.py:444
|
#: authentik/core/models.py:456
|
||||||
msgid ""
|
msgid ""
|
||||||
"Use the user's username, but deny enrollment when the username already "
|
"Use the user's username, but deny enrollment when the username already "
|
||||||
"exists."
|
"exists."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/core/models.py:451
|
#: authentik/core/models.py:463
|
||||||
msgid "Source's display Name."
|
msgid "Source's display Name."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/core/models.py:452
|
#: authentik/core/models.py:464
|
||||||
msgid "Internal source name, used in URLs."
|
msgid "Internal source name, used in URLs."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/core/models.py:471
|
#: authentik/core/models.py:483
|
||||||
msgid "Flow to use when authenticating existing users."
|
msgid "Flow to use when authenticating existing users."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/core/models.py:480
|
#: authentik/core/models.py:492
|
||||||
msgid "Flow to use when enrolling new users."
|
msgid "Flow to use when enrolling new users."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/core/models.py:488
|
#: authentik/core/models.py:500
|
||||||
msgid ""
|
msgid ""
|
||||||
"How the source determines if an existing user should be authenticated or a "
|
"How the source determines if an existing user should be authenticated or a "
|
||||||
"new user enrolled."
|
"new user enrolled."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/core/models.py:660
|
#: authentik/core/models.py:672
|
||||||
msgid "Token"
|
msgid "Token"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/core/models.py:661
|
#: authentik/core/models.py:673
|
||||||
msgid "Tokens"
|
msgid "Tokens"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/core/models.py:702
|
#: authentik/core/models.py:714
|
||||||
msgid "Property Mapping"
|
msgid "Property Mapping"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/core/models.py:703
|
#: authentik/core/models.py:715
|
||||||
msgid "Property Mappings"
|
msgid "Property Mappings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/core/models.py:738
|
#: authentik/core/models.py:750
|
||||||
msgid "Authenticated Session"
|
msgid "Authenticated Session"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: authentik/core/models.py:739
|
#: authentik/core/models.py:751
|
||||||
msgid "Authenticated Sessions"
|
msgid "Authenticated Sessions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
16
schema.yml
16
schema.yml
|
@ -31714,7 +31714,7 @@ components:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
readOnly: true
|
readOnly: true
|
||||||
users:
|
internal_users:
|
||||||
type: integer
|
type: integer
|
||||||
readOnly: true
|
readOnly: true
|
||||||
external_users:
|
external_users:
|
||||||
|
@ -31723,27 +31723,27 @@ components:
|
||||||
required:
|
required:
|
||||||
- expiry
|
- expiry
|
||||||
- external_users
|
- external_users
|
||||||
|
- internal_users
|
||||||
- key
|
- key
|
||||||
- license_uuid
|
- license_uuid
|
||||||
- name
|
- name
|
||||||
- users
|
|
||||||
LicenseForecast:
|
LicenseForecast:
|
||||||
type: object
|
type: object
|
||||||
description: Serializer for license forecast
|
description: Serializer for license forecast
|
||||||
properties:
|
properties:
|
||||||
users:
|
internal_users:
|
||||||
type: integer
|
type: integer
|
||||||
external_users:
|
external_users:
|
||||||
type: integer
|
type: integer
|
||||||
forecasted_users:
|
forecasted_internal_users:
|
||||||
type: integer
|
type: integer
|
||||||
forecasted_external_users:
|
forecasted_external_users:
|
||||||
type: integer
|
type: integer
|
||||||
required:
|
required:
|
||||||
- external_users
|
- external_users
|
||||||
- forecasted_external_users
|
- forecasted_external_users
|
||||||
- forecasted_users
|
- forecasted_internal_users
|
||||||
- users
|
- internal_users
|
||||||
LicenseRequest:
|
LicenseRequest:
|
||||||
type: object
|
type: object
|
||||||
description: License Serializer
|
description: License Serializer
|
||||||
|
@ -31757,7 +31757,7 @@ components:
|
||||||
type: object
|
type: object
|
||||||
description: Serializer for license status
|
description: Serializer for license status
|
||||||
properties:
|
properties:
|
||||||
users:
|
internal_users:
|
||||||
type: integer
|
type: integer
|
||||||
external_users:
|
external_users:
|
||||||
type: integer
|
type: integer
|
||||||
|
@ -31777,11 +31777,11 @@ components:
|
||||||
required:
|
required:
|
||||||
- external_users
|
- external_users
|
||||||
- has_license
|
- has_license
|
||||||
|
- internal_users
|
||||||
- latest_valid
|
- latest_valid
|
||||||
- read_only
|
- read_only
|
||||||
- show_admin_warning
|
- show_admin_warning
|
||||||
- show_user_warning
|
- show_user_warning
|
||||||
- users
|
|
||||||
- valid
|
- valid
|
||||||
Link:
|
Link:
|
||||||
type: object
|
type: object
|
||||||
|
|
|
@ -170,11 +170,11 @@ export class EnterpriseLicenseListPage extends TablePage<License> {
|
||||||
icon="pf-icon pf-icon-user"
|
icon="pf-icon pf-icon-user"
|
||||||
header=${msg("Forecast internal users")}
|
header=${msg("Forecast internal users")}
|
||||||
subtext=${msg(
|
subtext=${msg(
|
||||||
str`Estimated user count one year from now based on ${this.forecast?.users} current internal users and ${this.forecast?.forecastedUsers} forecasted internal users.`,
|
str`Estimated user count one year from now based on ${this.forecast?.internalUsers} current internal users and ${this.forecast?.forecastedInternalUsers} forecasted internal users.`,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
~ ${(this.forecast?.users || 0) +
|
~ ${(this.forecast?.internalUsers || 0) +
|
||||||
(this.forecast?.forecastedUsers || 0)}
|
(this.forecast?.forecastedInternalUsers || 0)}
|
||||||
</ak-aggregate-card>
|
</ak-aggregate-card>
|
||||||
<ak-aggregate-card
|
<ak-aggregate-card
|
||||||
class="pf-l-grid__item"
|
class="pf-l-grid__item"
|
||||||
|
@ -217,10 +217,8 @@ export class EnterpriseLicenseListPage extends TablePage<License> {
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
html`<div>${item.name}</div>`,
|
html`<div>${item.name}</div>`,
|
||||||
html`<div>
|
html`<div>${msg(str`Internal: ${item.internalUsers}`)}</div>
|
||||||
<small>0 / ${item.users}</small>
|
<div>${msg(str`External: ${item.externalUsers}`)}</div>`,
|
||||||
<small>0 / ${item.externalUsers}</small>
|
|
||||||
</div>`,
|
|
||||||
html`<ak-label color=${color}> ${item.expiry?.toLocaleString()} </ak-label>`,
|
html`<ak-label color=${color}> ${item.expiry?.toLocaleString()} </ak-label>`,
|
||||||
html`<ak-forms-modal>
|
html`<ak-forms-modal>
|
||||||
<span slot="submit"> ${msg("Update")} </span>
|
<span slot="submit"> ${msg("Update")} </span>
|
||||||
|
|
|
@ -5806,7 +5806,7 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||||
<source>Forecast internal users</source>
|
<source>Forecast internal users</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="sde9a3f41977ec1f8">
|
<trans-unit id="sde9a3f41977ec1f8">
|
||||||
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
|
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s4557b6b9da258643">
|
<trans-unit id="s4557b6b9da258643">
|
||||||
<source>Forecast external users</source>
|
<source>Forecast external users</source>
|
||||||
|
@ -5888,6 +5888,12 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s6931695c4f563bc4">
|
<trans-unit id="s6931695c4f563bc4">
|
||||||
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
|
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s0dd031b58ed4017c">
|
||||||
|
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s57b07e524f8f5c2a">
|
||||||
|
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
|
|
|
@ -6122,7 +6122,7 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||||
<source>Forecast internal users</source>
|
<source>Forecast internal users</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="sde9a3f41977ec1f8">
|
<trans-unit id="sde9a3f41977ec1f8">
|
||||||
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
|
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s4557b6b9da258643">
|
<trans-unit id="s4557b6b9da258643">
|
||||||
<source>Forecast external users</source>
|
<source>Forecast external users</source>
|
||||||
|
@ -6204,6 +6204,12 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s6931695c4f563bc4">
|
<trans-unit id="s6931695c4f563bc4">
|
||||||
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
|
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s0dd031b58ed4017c">
|
||||||
|
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s57b07e524f8f5c2a">
|
||||||
|
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
|
|
|
@ -5714,7 +5714,7 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||||
<source>Forecast internal users</source>
|
<source>Forecast internal users</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="sde9a3f41977ec1f8">
|
<trans-unit id="sde9a3f41977ec1f8">
|
||||||
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
|
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s4557b6b9da258643">
|
<trans-unit id="s4557b6b9da258643">
|
||||||
<source>Forecast external users</source>
|
<source>Forecast external users</source>
|
||||||
|
@ -5796,6 +5796,12 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s6931695c4f563bc4">
|
<trans-unit id="s6931695c4f563bc4">
|
||||||
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
|
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s0dd031b58ed4017c">
|
||||||
|
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s57b07e524f8f5c2a">
|
||||||
|
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
|
|
|
@ -5821,7 +5821,7 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||||
<source>Forecast internal users</source>
|
<source>Forecast internal users</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="sde9a3f41977ec1f8">
|
<trans-unit id="sde9a3f41977ec1f8">
|
||||||
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
|
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s4557b6b9da258643">
|
<trans-unit id="s4557b6b9da258643">
|
||||||
<source>Forecast external users</source>
|
<source>Forecast external users</source>
|
||||||
|
@ -5903,6 +5903,12 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s6931695c4f563bc4">
|
<trans-unit id="s6931695c4f563bc4">
|
||||||
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
|
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s0dd031b58ed4017c">
|
||||||
|
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s57b07e524f8f5c2a">
|
||||||
|
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
|
|
|
@ -5953,7 +5953,7 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||||
<source>Forecast internal users</source>
|
<source>Forecast internal users</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="sde9a3f41977ec1f8">
|
<trans-unit id="sde9a3f41977ec1f8">
|
||||||
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
|
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s4557b6b9da258643">
|
<trans-unit id="s4557b6b9da258643">
|
||||||
<source>Forecast external users</source>
|
<source>Forecast external users</source>
|
||||||
|
@ -6035,6 +6035,12 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s6931695c4f563bc4">
|
<trans-unit id="s6931695c4f563bc4">
|
||||||
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
|
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s0dd031b58ed4017c">
|
||||||
|
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s57b07e524f8f5c2a">
|
||||||
|
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
|
|
|
@ -6057,7 +6057,7 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||||
<source>Forecast internal users</source>
|
<source>Forecast internal users</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="sde9a3f41977ec1f8">
|
<trans-unit id="sde9a3f41977ec1f8">
|
||||||
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
|
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s4557b6b9da258643">
|
<trans-unit id="s4557b6b9da258643">
|
||||||
<source>Forecast external users</source>
|
<source>Forecast external users</source>
|
||||||
|
@ -6139,6 +6139,12 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s6931695c4f563bc4">
|
<trans-unit id="s6931695c4f563bc4">
|
||||||
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
|
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s0dd031b58ed4017c">
|
||||||
|
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s57b07e524f8f5c2a">
|
||||||
|
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
|
|
|
@ -5704,7 +5704,7 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||||
<source>Forecast internal users</source>
|
<source>Forecast internal users</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="sde9a3f41977ec1f8">
|
<trans-unit id="sde9a3f41977ec1f8">
|
||||||
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
|
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s4557b6b9da258643">
|
<trans-unit id="s4557b6b9da258643">
|
||||||
<source>Forecast external users</source>
|
<source>Forecast external users</source>
|
||||||
|
@ -5786,6 +5786,12 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s6931695c4f563bc4">
|
<trans-unit id="s6931695c4f563bc4">
|
||||||
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
|
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s0dd031b58ed4017c">
|
||||||
|
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s57b07e524f8f5c2a">
|
||||||
|
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
|
|
|
@ -7658,8 +7658,8 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||||
<target>预测内部用户</target>
|
<target>预测内部用户</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="sde9a3f41977ec1f8">
|
<trans-unit id="sde9a3f41977ec1f8">
|
||||||
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
|
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
|
||||||
<target>根据当前 <x id="0" equiv-text="${this.forecast?.users}"/> 名内部用户和 <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> 名预测的内部用户,估算从此时起一年后的用户数。</target>
|
<target>根据当前 <x id="0" equiv-text="${this.forecast?.internalUsers}"/> 名内部用户和 <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> 名预测的内部用户,估算从此时起一年后的用户数。</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s4557b6b9da258643">
|
<trans-unit id="s4557b6b9da258643">
|
||||||
<source>Forecast external users</source>
|
<source>Forecast external users</source>
|
||||||
|
@ -7765,6 +7765,12 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s6931695c4f563bc4">
|
<trans-unit id="s6931695c4f563bc4">
|
||||||
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
|
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s0dd031b58ed4017c">
|
||||||
|
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s57b07e524f8f5c2a">
|
||||||
|
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
|
|
|
@ -5759,7 +5759,7 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||||
<source>Forecast internal users</source>
|
<source>Forecast internal users</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="sde9a3f41977ec1f8">
|
<trans-unit id="sde9a3f41977ec1f8">
|
||||||
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
|
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s4557b6b9da258643">
|
<trans-unit id="s4557b6b9da258643">
|
||||||
<source>Forecast external users</source>
|
<source>Forecast external users</source>
|
||||||
|
@ -5841,6 +5841,12 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s6931695c4f563bc4">
|
<trans-unit id="s6931695c4f563bc4">
|
||||||
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
|
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s0dd031b58ed4017c">
|
||||||
|
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s57b07e524f8f5c2a">
|
||||||
|
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
|
|
|
@ -5758,7 +5758,7 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||||
<source>Forecast internal users</source>
|
<source>Forecast internal users</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="sde9a3f41977ec1f8">
|
<trans-unit id="sde9a3f41977ec1f8">
|
||||||
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
|
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s4557b6b9da258643">
|
<trans-unit id="s4557b6b9da258643">
|
||||||
<source>Forecast external users</source>
|
<source>Forecast external users</source>
|
||||||
|
@ -5840,6 +5840,12 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s6931695c4f563bc4">
|
<trans-unit id="s6931695c4f563bc4">
|
||||||
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
|
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s0dd031b58ed4017c">
|
||||||
|
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="s57b07e524f8f5c2a">
|
||||||
|
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
|
|
Reference in New Issue