outposts
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
This commit is contained in:
@ -212,14 +212,14 @@ def apply_blueprint(instance_pk: UUID):
|
|||||||
if not valid:
|
if not valid:
|
||||||
instance.status = BlueprintInstanceStatus.ERROR
|
instance.status = BlueprintInstanceStatus.ERROR
|
||||||
instance.save()
|
instance.save()
|
||||||
self.error(*logs)
|
self.logs(logs)
|
||||||
return
|
return
|
||||||
with capture_logs() as logs:
|
with capture_logs() as logs:
|
||||||
applied = importer.apply()
|
applied = importer.apply()
|
||||||
if not applied:
|
if not applied:
|
||||||
instance.status = BlueprintInstanceStatus.ERROR
|
instance.status = BlueprintInstanceStatus.ERROR
|
||||||
instance.save()
|
instance.save()
|
||||||
self.error(*logs)
|
self.logs(logs)
|
||||||
return
|
return
|
||||||
instance.status = BlueprintInstanceStatus.SUCCESSFUL
|
instance.status = BlueprintInstanceStatus.SUCCESSFUL
|
||||||
instance.last_applied_hash = file_hash
|
instance.last_applied_hash = file_hash
|
||||||
|
|||||||
@ -101,7 +101,13 @@ class KubernetesController(BaseController):
|
|||||||
all_logs = []
|
all_logs = []
|
||||||
for reconcile_key in self.reconcile_order:
|
for reconcile_key in self.reconcile_order:
|
||||||
if reconcile_key in self.outpost.config.kubernetes_disabled_components:
|
if reconcile_key in self.outpost.config.kubernetes_disabled_components:
|
||||||
all_logs += [f"{reconcile_key.title()}: Disabled"]
|
all_logs.append(
|
||||||
|
LogEvent(
|
||||||
|
log_level="info",
|
||||||
|
event=f"{reconcile_key.title()}: Disabled",
|
||||||
|
logger=str(type(self)),
|
||||||
|
)
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
with capture_logs() as logs:
|
with capture_logs() as logs:
|
||||||
reconciler_cls = self.reconcilers.get(reconcile_key)
|
reconciler_cls = self.reconcilers.get(reconcile_key)
|
||||||
@ -134,7 +140,13 @@ class KubernetesController(BaseController):
|
|||||||
all_logs = []
|
all_logs = []
|
||||||
for reconcile_key in self.reconcile_order:
|
for reconcile_key in self.reconcile_order:
|
||||||
if reconcile_key in self.outpost.config.kubernetes_disabled_components:
|
if reconcile_key in self.outpost.config.kubernetes_disabled_components:
|
||||||
all_logs += [f"{reconcile_key.title()}: Disabled"]
|
all_logs.append(
|
||||||
|
LogEvent(
|
||||||
|
log_level="info",
|
||||||
|
event=f"{reconcile_key.title()}: Disabled",
|
||||||
|
logger=str(type(self)),
|
||||||
|
)
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
with capture_logs() as logs:
|
with capture_logs() as logs:
|
||||||
reconciler_cls = self.reconcilers.get(reconcile_key)
|
reconciler_cls = self.reconcilers.get(reconcile_key)
|
||||||
|
|||||||
@ -1,37 +1,28 @@
|
|||||||
"""authentik outpost signals"""
|
"""authentik outpost signals"""
|
||||||
|
|
||||||
from django.contrib.auth.signals import user_logged_out
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from django.db.models.signals import m2m_changed, post_save, pre_delete, pre_save
|
from django.db.models.signals import m2m_changed, post_save, pre_delete, pre_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.http import HttpRequest
|
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.brands.models import Brand
|
from authentik.brands.models import Brand
|
||||||
from authentik.core.models import AuthenticatedSession, Provider, User
|
from authentik.core.models import AuthenticatedSession, Provider
|
||||||
from authentik.crypto.models import CertificateKeyPair
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
from authentik.lib.utils.reflection import class_to_path
|
from authentik.lib.utils.reflection import class_to_path
|
||||||
from authentik.outposts.models import Outpost, OutpostServiceConnection
|
from authentik.outposts.models import Outpost, OutpostServiceConnection
|
||||||
from authentik.outposts.tasks import (
|
from authentik.outposts.tasks import (
|
||||||
CACHE_KEY_OUTPOST_DOWN,
|
CACHE_KEY_OUTPOST_DOWN,
|
||||||
outpost_controller,
|
outpost_controller,
|
||||||
outpost_post_save,
|
|
||||||
outpost_session_end,
|
outpost_session_end,
|
||||||
|
outposts_and_related_update_dispatch,
|
||||||
)
|
)
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
UPDATE_TRIGGERING_MODELS = (
|
|
||||||
Outpost,
|
|
||||||
OutpostServiceConnection,
|
|
||||||
Provider,
|
|
||||||
CertificateKeyPair,
|
|
||||||
Brand,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_save, sender=Outpost)
|
@receiver(pre_save, sender=Outpost)
|
||||||
def pre_save_outpost(sender, instance: Outpost, **_):
|
def outpost_pre_save(sender, instance: Outpost, **_):
|
||||||
"""Pre-save checks for an outpost, if the name or config.kubernetes_namespace changes,
|
"""Pre-save checks for an outpost, if the name or config.kubernetes_namespace changes,
|
||||||
we call down and then wait for the up after save"""
|
we call down and then wait for the up after save"""
|
||||||
old_instances = Outpost.objects.filter(pk=instance.pk)
|
old_instances = Outpost.objects.filter(pk=instance.pk)
|
||||||
@ -54,55 +45,39 @@ def pre_save_outpost(sender, instance: Outpost, **_):
|
|||||||
|
|
||||||
|
|
||||||
@receiver(m2m_changed, sender=Outpost.providers.through)
|
@receiver(m2m_changed, sender=Outpost.providers.through)
|
||||||
def m2m_changed_update(sender, instance: Model, action: str, **_):
|
def outpost_m2m_changed(sender, instance: Model, action: str, **_):
|
||||||
"""Update outpost on m2m change, when providers are added or removed"""
|
"""Update outpost on m2m change, when providers are added or removed"""
|
||||||
if action in ["post_add", "post_remove", "post_clear"]:
|
if action in ["post_add", "post_remove", "post_clear"]:
|
||||||
outpost_post_save.send_with_options(
|
outposts_and_related_update_dispatch.send(class_to_path(instance.__class__), instance.pk)
|
||||||
args=(class_to_path(instance.__class__), instance.pk),
|
|
||||||
# TODO: how do we get the outpost here, if it makes sense
|
|
||||||
rel_obj=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save)
|
def outposts_and_related_post_save(sender, instance: Model, created: bool, **_):
|
||||||
def post_save_update(sender, instance: Model, created: bool, **_):
|
|
||||||
"""If an Outpost is saved, Ensure that token is created/updated
|
"""If an Outpost is saved, Ensure that token is created/updated
|
||||||
|
|
||||||
If an OutpostModel, or a model that is somehow connected to an OutpostModel is saved,
|
If an OutpostModel, or a model that is somehow connected to an OutpostModel is saved,
|
||||||
we send a message down the relevant OutpostModels WS connection to trigger an update"""
|
we send a message down the relevant OutpostModels WS connection to trigger an update"""
|
||||||
if instance.__module__ == "django.db.migrations.recorder":
|
|
||||||
return
|
|
||||||
if instance.__module__ == "__fake__":
|
|
||||||
return
|
|
||||||
if not isinstance(instance, UPDATE_TRIGGERING_MODELS):
|
|
||||||
return
|
|
||||||
if isinstance(instance, Outpost) and created:
|
if isinstance(instance, Outpost) and created:
|
||||||
LOGGER.info("New outpost saved, ensuring initial token and user are created")
|
LOGGER.info("New outpost saved, ensuring initial token and user are created")
|
||||||
_ = instance.token
|
_ = instance.token
|
||||||
outpost_post_save.send_with_options(
|
outposts_and_related_update_dispatch.send(class_to_path(instance.__class__), instance.pk)
|
||||||
args=(class_to_path(instance.__class__), instance.pk),
|
|
||||||
# TODO: how do we get the outpost here, if it makes sense
|
|
||||||
rel_obj=None,
|
post_save.connect(outposts_and_related_post_save, sender=Outpost, weak=False)
|
||||||
)
|
post_save.connect(outposts_and_related_post_save, sender=OutpostServiceConnection, weak=False)
|
||||||
|
post_save.connect(outposts_and_related_post_save, sender=Provider, weak=False)
|
||||||
|
post_save.connect(outposts_and_related_post_save, sender=CertificateKeyPair, weak=False)
|
||||||
|
post_save.connect(outposts_and_related_post_save, sender=Brand, weak=False)
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_delete, sender=Outpost)
|
@receiver(pre_delete, sender=Outpost)
|
||||||
def pre_delete_cleanup(sender, instance: Outpost, **_):
|
def outpost_pre_delete_cleanup(sender, instance: Outpost, **_):
|
||||||
"""Ensure that Outpost's user is deleted (which will delete the token through cascade)"""
|
"""Ensure that Outpost's user is deleted (which will delete the token through cascade)"""
|
||||||
instance.user.delete()
|
instance.user.delete()
|
||||||
cache.set(CACHE_KEY_OUTPOST_DOWN % instance.pk.hex, instance)
|
cache.set(CACHE_KEY_OUTPOST_DOWN % instance.pk.hex, instance)
|
||||||
outpost_controller.send(instance.pk.hex, action="down", from_cache=True)
|
outpost_controller.send(instance.pk.hex, action="down", from_cache=True)
|
||||||
|
|
||||||
|
|
||||||
@receiver(user_logged_out)
|
|
||||||
def logout_revoke_direct(sender: type[User], request: HttpRequest, **_):
|
|
||||||
"""Catch logout by direct logout and forward to providers"""
|
|
||||||
if not request.session or not request.session.session_key:
|
|
||||||
return
|
|
||||||
outpost_session_end.send(request.session.session_key)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_delete, sender=AuthenticatedSession)
|
@receiver(pre_delete, sender=AuthenticatedSession)
|
||||||
def logout_revoke(sender: type[AuthenticatedSession], instance: AuthenticatedSession, **_):
|
def outpost_logout_revoke(sender: type[AuthenticatedSession], instance: AuthenticatedSession, **_):
|
||||||
"""Catch logout by expiring sessions being deleted"""
|
"""Catch logout by expiring sessions being deleted"""
|
||||||
outpost_session_end.send(instance.session.session_key)
|
outpost_session_end.send(instance.session.session_key)
|
||||||
|
|||||||
@ -138,8 +138,7 @@ def outpost_controller(outpost_pk: str, action: str = "up", from_cache: bool = F
|
|||||||
else:
|
else:
|
||||||
if from_cache:
|
if from_cache:
|
||||||
cache.delete(CACHE_KEY_OUTPOST_DOWN % outpost_pk)
|
cache.delete(CACHE_KEY_OUTPOST_DOWN % outpost_pk)
|
||||||
for log in logs:
|
self.logs(logs)
|
||||||
self.info(log)
|
|
||||||
|
|
||||||
|
|
||||||
@actor(description=_("Ensure that all Outposts have valid Service Accounts and Tokens."))
|
@actor(description=_("Ensure that all Outposts have valid Service Accounts and Tokens."))
|
||||||
@ -155,17 +154,18 @@ def outpost_token_ensurer():
|
|||||||
self.info(f"Successfully checked {len(all_outposts)} Outposts.")
|
self.info(f"Successfully checked {len(all_outposts)} Outposts.")
|
||||||
|
|
||||||
|
|
||||||
@actor(description=_("If an Outpost is saved, ensure that token is created/updated."))
|
@actor(description=_("Dispatch tasks to update outposts when related objects are updated."))
|
||||||
def outpost_post_save(model_class: str, model_pk: Any):
|
def outposts_and_related_update_dispatch(model_class: str, pk: Any):
|
||||||
"""If an Outpost is saved, Ensure that token is created/updated
|
"""If an Outpost is saved, Ensure that token is created/updated
|
||||||
|
|
||||||
If an OutpostModel, or a model that is somehow connected to an OutpostModel is saved,
|
If an OutpostModel, or a model that is somehow connected to an OutpostModel is saved,
|
||||||
we send a message down the relevant OutpostModels WS connection to trigger an update"""
|
we send a message down the relevant OutpostModels WS connection to trigger an update"""
|
||||||
|
|
||||||
model: Model = path_to_class(model_class)
|
model: Model = path_to_class(model_class)
|
||||||
try:
|
try:
|
||||||
instance = model.objects.get(pk=model_pk)
|
instance = model.objects.get(pk=pk)
|
||||||
except model.DoesNotExist:
|
except model.DoesNotExist:
|
||||||
LOGGER.warning("Model does not exist", model=model, pk=model_pk)
|
LOGGER.warning("Model does not exist", model=model, pk=pk)
|
||||||
return
|
return
|
||||||
|
|
||||||
if isinstance(instance, Outpost):
|
if isinstance(instance, Outpost):
|
||||||
@ -175,7 +175,7 @@ def outpost_post_save(model_class: str, model_pk: Any):
|
|||||||
|
|
||||||
if isinstance(instance, OutpostModel | Outpost):
|
if isinstance(instance, OutpostModel | Outpost):
|
||||||
LOGGER.debug("triggering outpost update from outpostmodel/outpost", instance=instance)
|
LOGGER.debug("triggering outpost update from outpostmodel/outpost", instance=instance)
|
||||||
outpost_send_update(instance)
|
outposts_and_related_send_update(instance)
|
||||||
|
|
||||||
if isinstance(instance, OutpostServiceConnection):
|
if isinstance(instance, OutpostServiceConnection):
|
||||||
LOGGER.debug("triggering ServiceConnection state update", instance=instance)
|
LOGGER.debug("triggering ServiceConnection state update", instance=instance)
|
||||||
@ -200,27 +200,29 @@ def outpost_post_save(model_class: str, model_pk: Any):
|
|||||||
# Because the Outpost Model has an M2M to Provider,
|
# Because the Outpost Model has an M2M to Provider,
|
||||||
# we have to iterate over the entire QS
|
# we have to iterate over the entire QS
|
||||||
for reverse in getattr(instance, field_name).all():
|
for reverse in getattr(instance, field_name).all():
|
||||||
outpost_send_update(reverse)
|
outposts_and_related_send_update(reverse)
|
||||||
|
|
||||||
|
|
||||||
def outpost_send_update(model_instance: Model):
|
def outposts_and_related_send_update(model_instance: Model):
|
||||||
"""Send outpost update to all registered outposts, regardless to which authentik
|
"""Send outpost update to all related outposts"""
|
||||||
instance they are connected"""
|
|
||||||
channel_layer = get_channel_layer()
|
|
||||||
if isinstance(model_instance, OutpostModel):
|
if isinstance(model_instance, OutpostModel):
|
||||||
for outpost in model_instance.outpost_set.all():
|
for outpost in model_instance.outpost_set.all():
|
||||||
_outpost_single_update(outpost, channel_layer)
|
outpost_send_update.send_with_options(args=(outpost.pk,), rel_obj=outpost)
|
||||||
elif isinstance(model_instance, Outpost):
|
elif isinstance(model_instance, Outpost):
|
||||||
_outpost_single_update(model_instance, channel_layer)
|
outpost = model_instance
|
||||||
|
outpost_send_update.send_with_options(args=(outpost.pk,), rel_obj=outpost)
|
||||||
|
|
||||||
|
|
||||||
def _outpost_single_update(outpost: Outpost, layer=None):
|
@actor(description=_("Send update to outpost"))
|
||||||
"""Update outpost instances connected to a single outpost"""
|
def outpost_send_update(pk: Any):
|
||||||
|
"""Update outpost instance"""
|
||||||
|
outpost = Outpost.objects.filter(pk=pk).first()
|
||||||
|
if not outpost:
|
||||||
|
return
|
||||||
# Ensure token again, because this function is called when anything related to an
|
# Ensure token again, because this function is called when anything related to an
|
||||||
# OutpostModel is saved, so we can be sure permissions are right
|
# OutpostModel is saved, so we can be sure permissions are right
|
||||||
_ = outpost.token
|
_ = outpost.token
|
||||||
outpost.build_user_permissions(outpost.user)
|
outpost.build_user_permissions(outpost.user)
|
||||||
if not layer: # pragma: no cover
|
|
||||||
layer = get_channel_layer()
|
layer = get_channel_layer()
|
||||||
group = OUTPOST_GROUP % {"outpost_pk": str(outpost.pk)}
|
group = OUTPOST_GROUP % {"outpost_pk": str(outpost.pk)}
|
||||||
LOGGER.debug("sending update", channel=group, outpost=outpost)
|
LOGGER.debug("sending update", channel=group, outpost=outpost)
|
||||||
|
|||||||
@ -107,6 +107,10 @@ class Task(SerializerModel, TaskBase):
|
|||||||
)
|
)
|
||||||
return sanitize_item(log)
|
return sanitize_item(log)
|
||||||
|
|
||||||
|
def logs(self, logs: list[LogEvent]):
|
||||||
|
for log in logs:
|
||||||
|
self._messages.append(sanitize_item(log))
|
||||||
|
|
||||||
def log(
|
def log(
|
||||||
self,
|
self,
|
||||||
logger: str,
|
logger: str,
|
||||||
@ -117,7 +121,6 @@ class Task(SerializerModel, TaskBase):
|
|||||||
):
|
):
|
||||||
self._messages: list
|
self._messages: list
|
||||||
self._messages.append(
|
self._messages.append(
|
||||||
sanitize_item(
|
|
||||||
self._make_message(
|
self._make_message(
|
||||||
logger,
|
logger,
|
||||||
log_level,
|
log_level,
|
||||||
@ -125,7 +128,6 @@ class Task(SerializerModel, TaskBase):
|
|||||||
**attributes,
|
**attributes,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
if save:
|
if save:
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import "@goauthentik/admin/outposts/OutpostForm";
|
|||||||
import "@goauthentik/admin/outposts/OutpostHealth";
|
import "@goauthentik/admin/outposts/OutpostHealth";
|
||||||
import "@goauthentik/admin/outposts/OutpostHealthSimple";
|
import "@goauthentik/admin/outposts/OutpostHealthSimple";
|
||||||
import "@goauthentik/admin/rbac/ObjectPermissionModal";
|
import "@goauthentik/admin/rbac/ObjectPermissionModal";
|
||||||
|
import "@goauthentik/admin/system-tasks/ScheduleList";
|
||||||
|
import "@goauthentik/admin/system-tasks/TaskList";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { PFSize } from "@goauthentik/common/enums.js";
|
import { PFSize } from "@goauthentik/common/enums.js";
|
||||||
import { PFColor } from "@goauthentik/elements/Label";
|
import { PFColor } from "@goauthentik/elements/Label";
|
||||||
@ -24,6 +26,7 @@ import { ifDefined } from "lit/directives/if-defined.js";
|
|||||||
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
ModelEnum,
|
||||||
Outpost,
|
Outpost,
|
||||||
OutpostHealth,
|
OutpostHealth,
|
||||||
OutpostTypeEnum,
|
OutpostTypeEnum,
|
||||||
@ -163,6 +166,7 @@ export class OutpostListPage extends TablePage<Outpost> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderExpanded(item: Outpost): TemplateResult {
|
renderExpanded(item: Outpost): TemplateResult {
|
||||||
|
const [appLabel, modelName] = ModelEnum.AuthentikOutpostsOutpost.split(".");
|
||||||
return html`<td role="cell" colspan="5">
|
return html`<td role="cell" colspan="5">
|
||||||
<div class="pf-c-table__expandable-row-content">
|
<div class="pf-c-table__expandable-row-content">
|
||||||
<h3>
|
<h3>
|
||||||
@ -181,6 +185,38 @@ export class OutpostListPage extends TablePage<Outpost> {
|
|||||||
</div>`;
|
</div>`;
|
||||||
})}
|
})}
|
||||||
</dl>
|
</dl>
|
||||||
|
<dl class="pf-c-description-list pf-m-horizontal">
|
||||||
|
<div class="pf-c-description-list__group">
|
||||||
|
<dt class="pf-c-description-list__term">
|
||||||
|
<span class="pf-c-description-list__text">${msg("Schedules")}</span>
|
||||||
|
</dt>
|
||||||
|
<dd class="pf-c-description-list__description">
|
||||||
|
<div class="pf-c-description-list__text">
|
||||||
|
<ak-schedule-list
|
||||||
|
.relObjAppLabel=${appLabel}
|
||||||
|
.relObjModel=${modelName}
|
||||||
|
.relObjId="${item.pk}"
|
||||||
|
></ak-schedule-list>
|
||||||
|
</div>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
<dl class="pf-c-description-list pf-m-horizontal">
|
||||||
|
<div class="pf-c-description-list__group">
|
||||||
|
<dt class="pf-c-description-list__term">
|
||||||
|
<span class="pf-c-description-list__text">${msg("Tasks")}</span>
|
||||||
|
</dt>
|
||||||
|
<dd class="pf-c-description-list__description">
|
||||||
|
<div class="pf-c-description-list__text">
|
||||||
|
<ak-task-list
|
||||||
|
.relObjAppLabel=${appLabel}
|
||||||
|
.relObjModel=${modelName}
|
||||||
|
.relObjId="${item.pk}"
|
||||||
|
></ak-task-list>
|
||||||
|
</div>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</td>`;
|
</td>`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import "@goauthentik/admin/system-tasks/TaskList";
|
|||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import "@goauthentik/components/ak-status-label";
|
import "@goauthentik/components/ak-status-label";
|
||||||
import { PFColor } from "@goauthentik/elements/Label";
|
import { PFColor } from "@goauthentik/elements/Label";
|
||||||
import "@goauthentik/elements/Tabs";
|
|
||||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||||
import "@goauthentik/elements/forms/DeleteBulkForm";
|
import "@goauthentik/elements/forms/DeleteBulkForm";
|
||||||
import "@goauthentik/elements/forms/ModalForm";
|
import "@goauthentik/elements/forms/ModalForm";
|
||||||
@ -117,44 +116,38 @@ export class OutpostServiceConnectionListPage extends TablePage<ServiceConnectio
|
|||||||
const [appLabel, modelName] = item.metaModelName.split(".");
|
const [appLabel, modelName] = item.metaModelName.split(".");
|
||||||
return html` <td role="cell" colspan="5">
|
return html` <td role="cell" colspan="5">
|
||||||
<div class="pf-c-table__expandable-row-content">
|
<div class="pf-c-table__expandable-row-content">
|
||||||
<div class="pf-c-content">
|
<dl class="pf-c-description-list pf-m-horizontal">
|
||||||
<ak-tabs>
|
<div class="pf-c-description-list__group">
|
||||||
<section
|
<dt class="pf-c-description-list__term">
|
||||||
slot="page-schedules"
|
<span class="pf-c-description-list__text">${msg("Schedules")}</span>
|
||||||
data-tab-title="${msg("Schedules")}"
|
</dt>
|
||||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
<dd class="pf-c-description-list__description">
|
||||||
>
|
<div class="pf-c-description-list__text">
|
||||||
<div class="pf-l-grid pf-m-gutter">
|
|
||||||
<div
|
|
||||||
class="pf-l-grid__item pf-m-12-col pf-m-12-col-on-xl pf-m-12-col-on-2xl"
|
|
||||||
>
|
|
||||||
<ak-schedule-list
|
<ak-schedule-list
|
||||||
.relObjAppLabel=${appLabel}
|
.relObjAppLabel=${appLabel}
|
||||||
.relObjModel=${modelName}
|
.relObjModel=${modelName}
|
||||||
.relObjId="${item.pk}"
|
.relObjId="${item.pk}"
|
||||||
></ak-schedule-list>
|
></ak-schedule-list>
|
||||||
</div>
|
</div>
|
||||||
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</dl>
|
||||||
<section
|
<dl class="pf-c-description-list pf-m-horizontal">
|
||||||
slot="page-tasks"
|
<div class="pf-c-description-list__group">
|
||||||
data-tab-title="${msg("Tasks")}"
|
<dt class="pf-c-description-list__term">
|
||||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
<span class="pf-c-description-list__text">${msg("Tasks")}</span>
|
||||||
>
|
</dt>
|
||||||
<div class="pf-l-grid pf-m-gutter">
|
<dd class="pf-c-description-list__description">
|
||||||
<div
|
<div class="pf-c-description-list__text">
|
||||||
class="pf-l-grid__item pf-m-12-col pf-m-12-col-on-xl pf-m-12-col-on-2xl"
|
|
||||||
>
|
|
||||||
<ak-task-list
|
<ak-task-list
|
||||||
.relObjAppLabel=${appLabel}
|
.relObjAppLabel=${appLabel}
|
||||||
.relObjModel=${modelName}
|
.relObjModel=${modelName}
|
||||||
.relObjId="${item.pk}"
|
.relObjId="${item.pk}"
|
||||||
></ak-task-list>
|
></ak-task-list>
|
||||||
</div>
|
</div>
|
||||||
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</dl>
|
||||||
</ak-tabs>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</td>`;
|
</td>`;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user