Compare commits

...

2 Commits

Author SHA1 Message Date
Jens Langhammer
4ca8f032f4 add endpoint to start sync
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-05 22:12:30 +02:00
Jens Langhammer
97acd6288a split api
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-05 21:47:56 +02:00
12 changed files with 422 additions and 132 deletions

View File

View File

@@ -0,0 +1,35 @@
"""Source API Views"""
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.sources import (
GroupSourceConnectionSerializer,
GroupSourceConnectionViewSet,
UserSourceConnectionSerializer,
UserSourceConnectionViewSet,
)
from authentik.sources.ldap.models import (
GroupLDAPSourceConnection,
UserLDAPSourceConnection,
)
class UserLDAPSourceConnectionSerializer(UserSourceConnectionSerializer):
class Meta(UserSourceConnectionSerializer.Meta):
model = UserLDAPSourceConnection
class UserLDAPSourceConnectionViewSet(UserSourceConnectionViewSet, ModelViewSet):
queryset = UserLDAPSourceConnection.objects.all()
serializer_class = UserLDAPSourceConnectionSerializer
class GroupLDAPSourceConnectionSerializer(GroupSourceConnectionSerializer):
class Meta(GroupSourceConnectionSerializer.Meta):
model = GroupLDAPSourceConnection
class GroupLDAPSourceConnectionViewSet(GroupSourceConnectionViewSet, ModelViewSet):
queryset = GroupLDAPSourceConnection.objects.all()
serializer_class = GroupLDAPSourceConnectionSerializer

View File

@@ -0,0 +1,33 @@
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.property_mappings import PropertyMappingFilterSet, PropertyMappingSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.sources.ldap.models import (
LDAPSourcePropertyMapping,
)
class LDAPSourcePropertyMappingSerializer(PropertyMappingSerializer):
"""LDAP PropertyMapping Serializer"""
class Meta:
model = LDAPSourcePropertyMapping
fields = PropertyMappingSerializer.Meta.fields
class LDAPSourcePropertyMappingFilter(PropertyMappingFilterSet):
"""Filter for LDAPSourcePropertyMapping"""
class Meta(PropertyMappingFilterSet.Meta):
model = LDAPSourcePropertyMapping
class LDAPSourcePropertyMappingViewSet(UsedByMixin, ModelViewSet):
"""LDAP PropertyMapping Viewset"""
queryset = LDAPSourcePropertyMapping.objects.all()
serializer_class = LDAPSourcePropertyMappingSerializer
filterset_class = LDAPSourcePropertyMappingFilter
search_fields = ["name"]
ordering = ["name"]

View File

@@ -4,7 +4,7 @@ from typing import Any
from django.core.cache import cache
from django.utils.translation import gettext_lazy as _
from drf_spectacular.utils import extend_schema, inline_serializer
from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.fields import DictField, ListField, SerializerMethodField
@@ -13,23 +13,15 @@ from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.property_mappings import PropertyMappingFilterSet, PropertyMappingSerializer
from authentik.core.api.sources import (
GroupSourceConnectionSerializer,
GroupSourceConnectionViewSet,
SourceSerializer,
UserSourceConnectionSerializer,
UserSourceConnectionViewSet,
)
from authentik.core.api.used_by import UsedByMixin
from authentik.crypto.models import CertificateKeyPair
from authentik.lib.sync.api import SyncStatusSerializer
from authentik.rbac.filters import ObjectFilter
from authentik.sources.ldap.models import (
GroupLDAPSourceConnection,
LDAPSource,
LDAPSourcePropertyMapping,
UserLDAPSourceConnection,
)
from authentik.sources.ldap.tasks import CACHE_KEY_STATUS, SYNC_CLASSES, ldap_sync
from authentik.tasks.models import Task, TaskStatus
@@ -153,6 +145,25 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
search_fields = ["name", "slug"]
ordering = ["name"]
@extend_schema(
responses={
204: OpenApiResponse(description="Sync started"),
},
request=None,
)
@action(
methods=["POST"],
detail=True,
pagination_class=None,
url_path="sync/start",
filter_backends=[ObjectFilter],
)
def sync_start(self, request: Request, slug: str) -> Response:
"""Start source sync"""
source: LDAPSource = self.get_object()
ldap_sync.send(source.pk)
return Response(status=204)
@extend_schema(responses={200: SyncStatusSerializer()})
@action(
methods=["GET"],
@@ -162,7 +173,7 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
filter_backends=[ObjectFilter],
)
def sync_status(self, request: Request, slug: str) -> Response:
"""Get provider's sync status"""
"""Get sources's sync status"""
source: LDAPSource = self.get_object()
status = {}
@@ -224,48 +235,3 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
obj.pop("raw_dn", None)
all_objects[class_name].append(obj)
return Response(data=all_objects)
class LDAPSourcePropertyMappingSerializer(PropertyMappingSerializer):
"""LDAP PropertyMapping Serializer"""
class Meta:
model = LDAPSourcePropertyMapping
fields = PropertyMappingSerializer.Meta.fields
class LDAPSourcePropertyMappingFilter(PropertyMappingFilterSet):
"""Filter for LDAPSourcePropertyMapping"""
class Meta(PropertyMappingFilterSet.Meta):
model = LDAPSourcePropertyMapping
class LDAPSourcePropertyMappingViewSet(UsedByMixin, ModelViewSet):
"""LDAP PropertyMapping Viewset"""
queryset = LDAPSourcePropertyMapping.objects.all()
serializer_class = LDAPSourcePropertyMappingSerializer
filterset_class = LDAPSourcePropertyMappingFilter
search_fields = ["name"]
ordering = ["name"]
class UserLDAPSourceConnectionSerializer(UserSourceConnectionSerializer):
class Meta(UserSourceConnectionSerializer.Meta):
model = UserLDAPSourceConnection
class UserLDAPSourceConnectionViewSet(UserSourceConnectionViewSet, ModelViewSet):
queryset = UserLDAPSourceConnection.objects.all()
serializer_class = UserLDAPSourceConnectionSerializer
class GroupLDAPSourceConnectionSerializer(GroupSourceConnectionSerializer):
class Meta(GroupSourceConnectionSerializer.Meta):
model = GroupLDAPSourceConnection
class GroupLDAPSourceConnectionViewSet(GroupSourceConnectionViewSet, ModelViewSet):
queryset = GroupLDAPSourceConnection.objects.all()
serializer_class = GroupLDAPSourceConnectionSerializer

View File

@@ -159,7 +159,7 @@ class LDAPSource(IncomingSyncSource):
@property
def serializer(self) -> type[Serializer]:
from authentik.sources.ldap.api import LDAPSourceSerializer
from authentik.sources.ldap.api.sources import LDAPSourceSerializer
return LDAPSourceSerializer
@@ -356,7 +356,7 @@ class LDAPSourcePropertyMapping(PropertyMapping):
@property
def serializer(self) -> type[Serializer]:
from authentik.sources.ldap.api import LDAPSourcePropertyMappingSerializer
from authentik.sources.ldap.api.property_mappings import LDAPSourcePropertyMappingSerializer
return LDAPSourcePropertyMappingSerializer
@@ -377,7 +377,7 @@ class UserLDAPSourceConnection(UserSourceConnection):
@property
def serializer(self) -> type[Serializer]:
from authentik.sources.ldap.api import (
from authentik.sources.ldap.api.connections import (
UserLDAPSourceConnectionSerializer,
)
@@ -400,7 +400,7 @@ class GroupLDAPSourceConnection(GroupSourceConnection):
@property
def serializer(self) -> type[Serializer]:
from authentik.sources.ldap.api import (
from authentik.sources.ldap.api.connections import (
GroupLDAPSourceConnectionSerializer,
)

View File

@@ -1,11 +1,11 @@
"""API URLs"""
from authentik.sources.ldap.api import (
from authentik.sources.ldap.api.connections import (
GroupLDAPSourceConnectionViewSet,
LDAPSourcePropertyMappingViewSet,
LDAPSourceViewSet,
UserLDAPSourceConnectionViewSet,
)
from authentik.sources.ldap.api.property_mappings import LDAPSourcePropertyMappingViewSet
from authentik.sources.ldap.api.sources import LDAPSourceViewSet
api_urlpatterns = [
("propertymappings/source/ldap", LDAPSourcePropertyMappingViewSet),

View File

@@ -9492,6 +9492,119 @@ func (a *SourcesAPIService) SourcesLdapRetrieveExecute(r ApiSourcesLdapRetrieveR
return localVarReturnValue, localVarHTTPResponse, nil
}
type ApiSourcesLdapSyncStartCreateRequest struct {
ctx context.Context
ApiService *SourcesAPIService
slug string
}
func (r ApiSourcesLdapSyncStartCreateRequest) Execute() (*http.Response, error) {
return r.ApiService.SourcesLdapSyncStartCreateExecute(r)
}
/*
SourcesLdapSyncStartCreate Method for SourcesLdapSyncStartCreate
Start source sync
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
@param slug
@return ApiSourcesLdapSyncStartCreateRequest
*/
func (a *SourcesAPIService) SourcesLdapSyncStartCreate(ctx context.Context, slug string) ApiSourcesLdapSyncStartCreateRequest {
return ApiSourcesLdapSyncStartCreateRequest{
ApiService: a,
ctx: ctx,
slug: slug,
}
}
// Execute executes the request
func (a *SourcesAPIService) SourcesLdapSyncStartCreateExecute(r ApiSourcesLdapSyncStartCreateRequest) (*http.Response, error) {
var (
localVarHTTPMethod = http.MethodPost
localVarPostBody interface{}
formFiles []formFile
)
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "SourcesAPIService.SourcesLdapSyncStartCreate")
if err != nil {
return nil, &GenericOpenAPIError{error: err.Error()}
}
localVarPath := localBasePath + "/sources/ldap/{slug}/sync/start/"
localVarPath = strings.Replace(localVarPath, "{"+"slug"+"}", url.PathEscape(parameterValueToString(r.slug, "slug")), -1)
localVarHeaderParams := make(map[string]string)
localVarQueryParams := url.Values{}
localVarFormParams := url.Values{}
// to determine the Content-Type header
localVarHTTPContentTypes := []string{}
// set Content-Type header
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
if localVarHTTPContentType != "" {
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
}
// to determine the Accept header
localVarHTTPHeaderAccepts := []string{"application/json"}
// set Accept header
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
if localVarHTTPHeaderAccept != "" {
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
}
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
if err != nil {
return nil, err
}
localVarHTTPResponse, err := a.client.callAPI(req)
if err != nil || localVarHTTPResponse == nil {
return localVarHTTPResponse, err
}
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
localVarHTTPResponse.Body.Close()
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
if err != nil {
return localVarHTTPResponse, err
}
if localVarHTTPResponse.StatusCode >= 300 {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: localVarHTTPResponse.Status,
}
if localVarHTTPResponse.StatusCode == 400 {
var v ValidationError
err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr.error = err.Error()
return localVarHTTPResponse, newErr
}
newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
newErr.model = v
return localVarHTTPResponse, newErr
}
if localVarHTTPResponse.StatusCode == 403 {
var v GenericError
err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr.error = err.Error()
return localVarHTTPResponse, newErr
}
newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
newErr.model = v
}
return localVarHTTPResponse, newErr
}
return localVarHTTPResponse, nil
}
type ApiSourcesLdapSyncStatusRetrieveRequest struct {
ctx context.Context
ApiService *SourcesAPIService
@@ -9505,7 +9618,7 @@ func (r ApiSourcesLdapSyncStatusRetrieveRequest) Execute() (*SyncStatus, *http.R
/*
SourcesLdapSyncStatusRetrieve Method for SourcesLdapSyncStatusRetrieve
Get provider's sync status
Get sources's sync status
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
@param slug

View File

@@ -624,6 +624,15 @@ pub enum SourcesLdapRetrieveError {
UnknownValue(serde_json::Value),
}
/// struct for typed errors of method [`sources_ldap_sync_start_create`]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SourcesLdapSyncStartCreateError {
Status400(models::ValidationError),
Status403(models::GenericError),
UnknownValue(serde_json::Value),
}
/// struct for typed errors of method [`sources_ldap_sync_status_retrieve`]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
@@ -6172,7 +6181,49 @@ pub async fn sources_ldap_retrieve(
}
}
/// Get provider's sync status
/// Start source sync
pub async fn sources_ldap_sync_start_create(
configuration: &configuration::Configuration,
slug: &str,
) -> Result<(), Error<SourcesLdapSyncStartCreateError>> {
// add a prefix to parameters to efficiently prevent name collisions
let p_path_slug = slug;
let uri_str = format!(
"{}/sources/ldap/{slug}/sync/start/",
configuration.base_path,
slug = crate::apis::urlencode(p_path_slug)
);
let mut req_builder = configuration
.client
.request(reqwest::Method::POST, &uri_str);
if let Some(ref user_agent) = configuration.user_agent {
req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
}
if let Some(ref token) = configuration.bearer_access_token {
req_builder = req_builder.bearer_auth(token.to_owned());
};
let req = req_builder.build()?;
let resp = configuration.client.execute(req).await?;
let status = resp.status();
if !status.is_client_error() && !status.is_server_error() {
Ok(())
} else {
let content = resp.text().await?;
let entity: Option<SourcesLdapSyncStartCreateError> = serde_json::from_str(&content).ok();
Err(Error::ResponseError(ResponseContent {
status,
content,
entity,
}))
}
}
/// Get sources's sync status
pub async fn sources_ldap_sync_status_retrieve(
configuration: &configuration::Configuration,
slug: &str,

View File

@@ -601,6 +601,10 @@ export interface SourcesLdapRetrieveRequest {
slug: string;
}
export interface SourcesLdapSyncStartCreateRequest {
slug: string;
}
export interface SourcesLdapSyncStatusRetrieveRequest {
slug: string;
}
@@ -6076,6 +6080,69 @@ export class SourcesApi extends runtime.BaseAPI {
return await response.value();
}
/**
* Creates request options for sourcesLdapSyncStartCreate without sending the request
*/
async sourcesLdapSyncStartCreateRequestOpts(
requestParameters: SourcesLdapSyncStartCreateRequest,
): Promise<runtime.RequestOpts> {
if (requestParameters["slug"] == null) {
throw new runtime.RequiredError(
"slug",
'Required parameter "slug" was null or undefined when calling sourcesLdapSyncStartCreate().',
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
if (this.configuration && this.configuration.accessToken) {
const token = this.configuration.accessToken;
const tokenString = await token("authentik", []);
if (tokenString) {
headerParameters["Authorization"] = `Bearer ${tokenString}`;
}
}
let urlPath = `/sources/ldap/{slug}/sync/start/`;
urlPath = urlPath.replace(
`{${"slug"}}`,
encodeURIComponent(String(requestParameters["slug"])),
);
return {
path: urlPath,
method: "POST",
headers: headerParameters,
query: queryParameters,
};
}
/**
* Start source sync
*/
async sourcesLdapSyncStartCreateRaw(
requestParameters: SourcesLdapSyncStartCreateRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<runtime.ApiResponse<void>> {
const requestOptions = await this.sourcesLdapSyncStartCreateRequestOpts(requestParameters);
const response = await this.request(requestOptions, initOverrides);
return new runtime.VoidApiResponse(response);
}
/**
* Start source sync
*/
async sourcesLdapSyncStartCreate(
requestParameters: SourcesLdapSyncStartCreateRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<void> {
await this.sourcesLdapSyncStartCreateRaw(requestParameters, initOverrides);
}
/**
* Creates request options for sourcesLdapSyncStatusRetrieve without sending the request
*/
@@ -6117,7 +6184,7 @@ export class SourcesApi extends runtime.BaseAPI {
}
/**
* Get provider\'s sync status
* Get sources\'s sync status
*/
async sourcesLdapSyncStatusRetrieveRaw(
requestParameters: SourcesLdapSyncStatusRetrieveRequest,
@@ -6131,7 +6198,7 @@ export class SourcesApi extends runtime.BaseAPI {
}
/**
* Get provider\'s sync status
* Get sources\'s sync status
*/
async sourcesLdapSyncStatusRetrieve(
requestParameters: SourcesLdapSyncStatusRetrieveRequest,

View File

@@ -23422,10 +23422,32 @@ paths:
$ref: '#/components/responses/ValidationErrorResponse'
'403':
$ref: '#/components/responses/GenericErrorResponse'
/sources/ldap/{slug}/sync/start/:
post:
operationId: sources_ldap_sync_start_create
description: Start source sync
parameters:
- in: path
name: slug
schema:
type: string
description: Internal source name, used in URLs.
required: true
tags:
- sources
security:
- authentik: []
responses:
'204':
description: Sync started
'400':
$ref: '#/components/responses/ValidationErrorResponse'
'403':
$ref: '#/components/responses/GenericErrorResponse'
/sources/ldap/{slug}/sync/status/:
get:
operationId: sources_ldap_sync_status_retrieve
description: Get provider's sync status
description: Get sources's sync status
parameters:
- in: path
name: slug

View File

@@ -4,7 +4,7 @@ import { AKElement } from "#elements/Base";
import { SlottedTemplateResult } from "#elements/types";
import { msg } from "@lit/localize";
import { CSSResult, html, nothing } from "lit";
import { CSSResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFList from "@patternfly/patternfly/components/List/list.css";
@@ -12,17 +12,17 @@ import PFList from "@patternfly/patternfly/components/List/list.css";
@customElement("ak-source-ldap-connectivity")
export class LDAPSourceConnectivity extends AKElement {
@property()
connectivity?: {
connectivity: {
[key: string]: {
[key: string]: string;
};
};
} | null = null;
static styles: CSSResult[] = [PFList];
render(): SlottedTemplateResult {
if (!this.connectivity) {
return nothing;
return html`${msg("No connectivity status available.")}`;
}
return html`<ul class="pf-c-list">
${Object.keys(this.connectivity).map((serverKey) => {

View File

@@ -16,10 +16,12 @@ import { EVENT_REFRESH } from "#common/constants";
import { AKElement } from "#elements/Base";
import { SlottedTemplateResult } from "#elements/types";
import renderDescriptionList from "#components/DescriptionList";
import { LDAPSource, ModelEnum, SourcesApi } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { CSSResult, html, nothing } from "lit";
import { CSSResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
@@ -65,9 +67,6 @@ export class LDAPSourceViewPage extends AKElement {
}
render(): SlottedTemplateResult {
if (!this.source) {
return nothing;
}
const [appLabel, modelName] = ModelEnum.AuthentikSourcesLdapLdapsource.split(".");
return html`<main>
<ak-tabs>
@@ -83,66 +82,70 @@ export class LDAPSourceViewPage extends AKElement {
<div
class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-6-col-on-2xl"
>
<div class="pf-c-card__title">${msg("Info")}</div>
<div class="pf-c-card__body">
<dl class="pf-c-description-list pf-m-2-col-on-lg">
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${msg("Name")}</span
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${this.source.name}
</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${msg("Server URI")}</span
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${this.source.serverUri}
</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${msg("Base DN")}</span
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<ul>
<li>${this.source.baseDn}</li>
</ul>
</div>
</dd>
</div>
</dl>
</div>
<div class="pf-c-card__footer">
<ak-forms-modal>
<span slot="submit">${msg("Save Changes")}</span>
<span slot="header">${msg("Update LDAP Source")}</span>
<ak-source-ldap-form
slot="form"
.instancePk=${this.source.slug}
>
</ak-source-ldap-form>
<button slot="trigger" class="pf-c-button pf-m-primary">
${msg("Edit")}
</button>
</ak-forms-modal>
${renderDescriptionList(
[
[msg("Name"), html`${this.source.name}`],
[msg("Server URI"), html`${this.source.serverUri}`],
[msg("Base DN"), html`${this.source.baseDn}`],
[
msg("Status"),
html`<ak-status-label
type="neutral"
?good=${this.source?.enabled}
good-label=${msg("Enabled")}
bad-label=${msg("Disabled")}
></ak-status-label>`,
],
[
msg("Related actions"),
html`<ak-forms-modal>
<span slot="submit"
>${msg("Save Changes")}</span
>
<span slot="header"
>${msg("Update LDAP Source")}</span
>
<ak-source-ldap-form
slot="form"
.instancePk=${this.source.slug}
>
</ak-source-ldap-form>
<button
slot="trigger"
class="pf-c-button pf-m-primary pf-m-block"
>
${msg("Edit")}
</button>
</ak-forms-modal>
<ak-action-button
class="pf-m-secondary pf-m-block"
label=${msg("Start sync")}
.apiRequest=${() => {
return new SourcesApi(DEFAULT_CONFIG)
.sourcesLdapSyncStartCreate({
slug: this.source?.slug,
})
.then(() => {
this.dispatchEvent(
new CustomEvent(EVENT_REFRESH, {
bubbles: true,
composed: true,
}),
);
});
}}
>
${msg("Start sync")}
</ak-action-button>`,
],
],
{ twocolumn: true },
)}
</div>
</div>
<div
class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-6-col-on-2xl"
>
<div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-6-col-on-2xl">
<ak-sync-status-card
.fetch=${() => {
return new SourcesApi(