feat(connectors): data-access group picker for perm-synced connectors (#15033)

This commit is contained in:
Justin Tahara authored and GitHub committed 2026-09-24 22:38:06 +00:00
1 parent c8c8c04ce6
commit d29ce87d41
24 files changed
+447 -17

No files matched your search

+9
View File
@@ -814,6 +814,15 @@ def associate_credential_to_connector(
if metadata.access_type == AccessType.SYNC_RESTRICTED:
# Becomes creatable in the same change that enforces its data-access
# groups at query time, so no restricted pair exists without them.
# TODO(evan, ENG-4342): remove this rejection in the enforcement change,
# together with:
# - the allowed-connector query filter and the /chat/file check
# - the creation path: restriction_group_ids, validation, persistence
# (branch jtahara/connector-group-restrictions-creation-path)
# - SYNC-only checks in connector_credential_pair.py: listing
# visibility, tier/source validation, get_all_auto_sync_cc_pairs,
# get_cc_pairs_by_source
# - creating the pair and its data-access rows in one transaction
raise OnyxError(
OnyxErrorCode.FEATURE_NOT_AVAILABLE,
"Restricted perm-synced connectors are not available yet.",
@@ -132,6 +132,7 @@ from onyx.server.documents.models import (
ConnectorCredentialPairIdentifier,
ConnectorFileInfo,
ConnectorFilesResponse,
ConnectorGroupRestrictionsStatus,
ConnectorIndexingStatusLite,
ConnectorIndexingStatusLiteResponse,
ConnectorRequestSubmission,
@@ -226,6 +227,19 @@ def upsert_gmail_service_account_credential(
return ObjectCreationIdResponse(id=credential.id)
@router.get("/connector-group-restrictions")
def get_connector_group_restrictions_status(
_: User = Depends(
require_permission(Permission.MANAGE_CONNECTORS, allow_scope=True)
),
) -> ConnectorGroupRestrictionsStatus:
"""Whether connector forms offer the data-access group restriction. Scoped
managers read it here because the security settings API is admin-only."""
return ConnectorGroupRestrictionsStatus(
enabled=get_security_settings().allow_connector_group_restrictions
)
@router.get("/admin/connector/google-drive/check-auth/{credential_id}")
def check_drive_tokens(
credential_id: int,
+4
View File
@@ -823,6 +823,10 @@ class AuthStatus(BaseModel):
authenticated: bool
class ConnectorGroupRestrictionsStatus(BaseModel):
enabled: bool
class AuthUrl(BaseModel):
auth_url: str
@@ -36,6 +36,7 @@ import {
AccordionTrigger,
} from "@/components/ui/accordion";
import { CheckboxField } from "@/refresh-components/form/LabeledCheckboxField";
import { isPermSynced } from "@/lib/connectors/accessType";
export interface SlackChannelConfigFormFieldsProps {
isUpdate: boolean;
@@ -67,8 +68,8 @@ export function SlackChannelConfigFormFields({
// Helper function to check if a document set contains sync connectors
const documentSetContainsSync = (documentSet: DocumentSetSummary) => {
return documentSet.cc_pair_summaries.some(
(summary) => summary.access_type === "sync"
return documentSet.cc_pair_summaries.some((summary) =>
isPermSynced(summary.access_type)
);
};
@@ -8,6 +8,8 @@ import {
import { useField } from "formik";
import { useTranslations } from "next-intl";
import { AutoSyncOptions } from "./AutoSyncOptions";
import { ConnectorGroupRestrictionPicker } from "@/sections/connectors/ConnectorGroupRestrictionPicker";
import { useConnectorGroupRestrictionsEnabled } from "@/lib/connectors/hooks";
import { useTierAtLeast } from "@/hooks/useTierAtLeast";
import { Tier } from "@/lib/settings/types";
import { useEffect, useMemo } from "react";
@@ -42,6 +44,7 @@ export function AccessTypeForm({
// both are Business+ features.
const businessTier = useTierAtLeast(Tier.BUSINESS);
const showAutoSync = businessTier && isValidAutoSyncSource(connector);
const groupRestrictionsEnabled = useConnectorGroupRestrictionsEnabled();
const selectedAuthMethod = currentCredential?.credential_json?.[
"authentication_method"
@@ -151,6 +154,9 @@ export function AccessTypeForm({
}
includeDefault={false}
/>
{access_type.value === "sync" &&
showAutoSync &&
groupRestrictionsEnabled && <ConnectorGroupRestrictionPicker />}
{access_type.value === "sync" && showAutoSync && (
<AutoSyncOptions connectorType={connector as ValidAutoSyncSource} />
)}
+18 -1
View File
@@ -2152,12 +2152,29 @@
"title": "خطأ في مزامنة عضويات المجموعات"
}
},
"groupRestriction": {
"empty": {
"description": "يحتفظ الجميع بالوصول المتزامن من المصدر.",
"title": "لا توجد قيود حتى تتم إضافة مجموعات"
},
"memberCount": "{count, plural, zero {لا أعضاء} one {عضو واحد} two {عضوان} few {# أعضاء} many {# عضوًا} other {# عضو}}",
"picker": {
"placeholder": "أضف مجموعات لتقييد الوصول إلى هذا الموصل"
},
"remove": {
"tooltip": "إزالة {name}"
},
"toggle": {
"description": "قصر الوصول على أعضاء المجموعات المحددة. تظل الأذونات المتزامنة سارية.",
"title": "تقييد الوصول بالمجموعات"
}
},
"groupSelector": {
"assignGroupAccess": {
"title": "إسناد وصول المجموعات لهذا الموصّل"
},
"assignToGroup": {
"title": "إسناد هذا الموصّل إلى مجموعة"
"title": "المجموعات التي يمكنها إدارة هذا الموصل"
},
"scopedManagerGroups": {
"description": "حدد مجموعة أو أكثر من المجموعات التي تديرها لمنح حق الوصول إلى هذا الموصّل"
+18 -1
View File
@@ -2152,12 +2152,29 @@
"title": "Fehler bei der Synchronisierung der Gruppenmitgliedschaften"
}
},
"groupRestriction": {
"empty": {
"description": "Alle behalten den aus der Quelle synchronisierten Zugriff.",
"title": "Keine Beschränkung, bis Gruppen hinzugefügt werden"
},
"memberCount": "{count, plural, one {# Mitglied} other {# Mitglieder}}",
"picker": {
"placeholder": "Gruppen hinzufügen, um den Zugriff auf diesen Connector zu beschränken"
},
"remove": {
"tooltip": "{name} entfernen"
},
"toggle": {
"description": "Zugriff auf Mitglieder der ausgewählten Gruppen beschränken. Synchronisierte Berechtigungen gelten weiterhin.",
"title": "Zugriff auf Gruppen beschränken"
}
},
"groupSelector": {
"assignGroupAccess": {
"title": "Gruppenzugriff für diesen Konnektor zuweisen"
},
"assignToGroup": {
"title": "Diesen Konnektor einer Gruppe zuweisen"
"title": "Gruppen, die diesen Connector verwalten können"
},
"scopedManagerGroups": {
"description": "Wähle eine oder mehrere der von dir verwalteten Gruppen aus, um Zugriff auf diesen Konnektor zu gewähren"
+18 -1
View File
@@ -2152,12 +2152,29 @@
"title": "Group Membership Sync Error"
}
},
"groupRestriction": {
"empty": {
"description": "Everyone keeps the access synced from the source.",
"title": "No restriction until groups are added"
},
"memberCount": "{count, plural, one {# member} other {# members}}",
"picker": {
"placeholder": "Add groups to restrict access to this connector"
},
"remove": {
"tooltip": "Remove {name}"
},
"toggle": {
"description": "Limit access to members of selected groups. Synced permissions still apply.",
"title": "Restrict Access to Groups"
}
},
"groupSelector": {
"assignGroupAccess": {
"title": "Assign group access for this Connector"
},
"assignToGroup": {
"title": "Assign this Connector to a group"
"title": "Groups that can manage this Connector"
},
"scopedManagerGroups": {
"description": "Select one or more of the groups you manage to give access to this Connector"
+18 -1
View File
@@ -2152,12 +2152,29 @@
"title": "Error de sincronización de pertenencia a grupos"
}
},
"groupRestriction": {
"empty": {
"description": "Todos conservan el acceso sincronizado desde el origen.",
"title": "Sin restricción hasta que se añadan grupos"
},
"memberCount": "{count, plural, one {# miembro} other {# miembros}}",
"picker": {
"placeholder": "Añade grupos para restringir el acceso a este conector"
},
"remove": {
"tooltip": "Quitar {name}"
},
"toggle": {
"description": "Limita el acceso a los miembros de los grupos seleccionados. Los permisos sincronizados siguen aplicándose.",
"title": "Restringir acceso a grupos"
}
},
"groupSelector": {
"assignGroupAccess": {
"title": "Asignar acceso de grupos a este conector"
},
"assignToGroup": {
"title": "Asignar este conector a un grupo"
"title": "Grupos que pueden administrar este conector"
},
"scopedManagerGroups": {
"description": "Selecciona uno o más de los grupos que administras para dar acceso a este conector"
+18 -1
View File
@@ -2152,12 +2152,29 @@
"title": "Erreur de synchronisation de l'appartenance aux groupes"
}
},
"groupRestriction": {
"empty": {
"description": "Tout le monde conserve l'accès synchronisé depuis la source.",
"title": "Aucune restriction tant qu'aucun groupe n'est ajouté"
},
"memberCount": "{count, plural, one {# membre} other {# membres}}",
"picker": {
"placeholder": "Ajouter des groupes pour restreindre l'accès à ce connecteur"
},
"remove": {
"tooltip": "Retirer {name}"
},
"toggle": {
"description": "Limite l'accès aux membres des groupes sélectionnés. Les permissions synchronisées s'appliquent toujours.",
"title": "Restreindre l'accès à des groupes"
}
},
"groupSelector": {
"assignGroupAccess": {
"title": "Attribuer l'accès des groupes à ce connecteur"
},
"assignToGroup": {
"title": "Attribuer ce connecteur à un groupe"
"title": "Groupes pouvant gérer ce connecteur"
},
"scopedManagerGroups": {
"description": "Sélectionnez un ou plusieurs des groupes que vous gérez pour donner accès à ce connecteur"
+18 -1
View File
@@ -2152,12 +2152,29 @@
"title": "グループメンバーシップの同期エラー"
}
},
"groupRestriction": {
"empty": {
"description": "全員がソースから同期されたアクセス権を保持します。",
"title": "グループを追加するまで制限はありません"
},
"memberCount": "{count, plural, other {#人のメンバー}}",
"picker": {
"placeholder": "このコネクタへのアクセスを制限するグループを追加"
},
"remove": {
"tooltip": "{name}を削除"
},
"toggle": {
"description": "選択したグループのメンバーにアクセスを限定します。同期された権限も引き続き適用されます。",
"title": "グループへのアクセス制限"
}
},
"groupSelector": {
"assignGroupAccess": {
"title": "このコネクタへのグループアクセスを割り当てる"
},
"assignToGroup": {
"title": "このコネクタをグループに割り当てる"
"title": "このコネクタを管理できるグループ"
},
"scopedManagerGroups": {
"description": "このコネクタへのアクセスを付与するには、管理しているグループを1つ以上選択してください"
+18 -1
View File
@@ -2152,12 +2152,29 @@
"title": "그룹 멤버십 동기화 오류"
}
},
"groupRestriction": {
"empty": {
"description": "모든 사용자가 소스에서 동기화된 접근 권한을 유지합니다.",
"title": "그룹을 추가하기 전까지 제한 없음"
},
"memberCount": "{count, plural, other {멤버 #명}}",
"picker": {
"placeholder": "이 커넥터에 대한 접근을 제한할 그룹 추가"
},
"remove": {
"tooltip": "{name} 제거"
},
"toggle": {
"description": "선택한 그룹의 멤버로 접근을 제한합니다. 동기화된 권한은 계속 적용됩니다.",
"title": "그룹으로 접근 제한"
}
},
"groupSelector": {
"assignGroupAccess": {
"title": "이 커넥터에 대한 그룹 액세스 할당"
},
"assignToGroup": {
"title": "이 커넥터를 그룹에 할당"
"title": "이 커넥터를 관리할 수 있는 그룹"
},
"scopedManagerGroups": {
"description": "이 커넥터에 대한 액세스 권한을 부여하려면 관리 중인 그룹을 하나 이상 선택하세요"
+18 -1
View File
@@ -2152,12 +2152,29 @@
"title": "Erro na sincronização das associações de grupo"
}
},
"groupRestriction": {
"empty": {
"description": "Todos mantêm o acesso sincronizado da origem.",
"title": "Sem restrição até que grupos sejam adicionados"
},
"memberCount": "{count, plural, one {# membro} other {# membros}}",
"picker": {
"placeholder": "Adicione grupos para restringir o acesso a este conector"
},
"remove": {
"tooltip": "Remover {name}"
},
"toggle": {
"description": "Limite o acesso aos membros dos grupos selecionados. As permissões sincronizadas continuam valendo.",
"title": "Restringir acesso a grupos"
}
},
"groupSelector": {
"assignGroupAccess": {
"title": "Atribuir acesso de grupos a este conector"
},
"assignToGroup": {
"title": "Atribuir este conector a um grupo"
"title": "Grupos que podem gerenciar este conector"
},
"scopedManagerGroups": {
"description": "Selecione um ou mais dos grupos que você gerencia para dar acesso a este conector"
+18 -1
View File
@@ -2152,12 +2152,29 @@
"title": "用户组成员同步错误"
}
},
"groupRestriction": {
"empty": {
"description": "所有人保留从源系统同步的访问权限。",
"title": "添加群组前不设限制"
},
"memberCount": "{count, plural, other {# 名成员}}",
"picker": {
"placeholder": "添加群组以限制对此连接器的访问"
},
"remove": {
"tooltip": "移除 {name}"
},
"toggle": {
"description": "仅允许所选群组的成员访问。同步的权限仍然适用。",
"title": "限制为群组访问"
}
},
"groupSelector": {
"assignGroupAccess": {
"title": "为此连接器分配组访问权限"
},
"assignToGroup": {
"title": "将此连接器分配给一个组"
"title": "可以管理此连接器的群组"
},
"scopedManagerGroups": {
"description": "选择一个或多个您管理的组以授予对此连接器的访问权限"
+53
View File
@@ -0,0 +1,53 @@
import { isPermSynced, toWireAccess } from "@/lib/connectors/accessType";
describe("toWireAccess", () => {
it("restricts a synced connector when groups are chosen", () => {
expect(
toWireAccess("sync", {
restrict_access_to_groups: true,
restriction_group_ids: [3, 7],
})
).toEqual({
access_type: "sync_restricted",
restriction_group_ids: [3, 7],
});
});
it("keeps plain sync when the switch is on but no groups are chosen", () => {
expect(
toWireAccess("sync", {
restrict_access_to_groups: true,
restriction_group_ids: [],
})
).toEqual({ access_type: "sync", restriction_group_ids: [] });
});
it("drops stale groups when the switch is off", () => {
expect(
toWireAccess("sync", {
restrict_access_to_groups: false,
restriction_group_ids: [3],
})
).toEqual({ access_type: "sync", restriction_group_ids: [] });
});
it("never restricts non-synced access types", () => {
for (const accessType of ["public", "private"] as const) {
expect(
toWireAccess(accessType, {
restrict_access_to_groups: true,
restriction_group_ids: [3],
})
).toEqual({ access_type: accessType, restriction_group_ids: [] });
}
});
});
describe("isPermSynced", () => {
it("treats both synced types as synced", () => {
expect(isPermSynced("sync")).toBe(true);
expect(isPermSynced("sync_restricted")).toBe(true);
expect(isPermSynced("private")).toBe(false);
expect(isPermSynced("public")).toBe(false);
});
});
+38
View File
@@ -0,0 +1,38 @@
import type { AccessType } from "@/lib/types";
// Perm sync plus a data-access group restriction (ENG-4342). The wire name is
// provisional until the connector editing initiative confirms it.
export const SYNC_RESTRICTED_ACCESS_TYPE =
"sync_restricted" satisfies AccessType;
export function isPermSynced(accessType: AccessType): boolean {
return accessType === "sync" || accessType === SYNC_RESTRICTED_ACCESS_TYPE;
}
export interface ConnectorGroupRestrictionFormValues {
restrict_access_to_groups: boolean;
restriction_group_ids: number[];
}
export interface WireAccess {
access_type: AccessType;
restriction_group_ids: number[];
}
// The form keeps "sync" plus a restriction flag, so the dropdown never sees the
// fourth value. A switched-on restriction with no groups restricts nobody.
export function toWireAccess(
accessType: AccessType,
restriction: ConnectorGroupRestrictionFormValues
): WireAccess {
const restricted =
accessType === "sync" &&
restriction.restrict_access_to_groups &&
restriction.restriction_group_ids.length > 0;
return restricted
? {
access_type: SYNC_RESTRICTED_ACCESS_TYPE,
restriction_group_ids: restriction.restriction_group_ids,
}
: { access_type: accessType, restriction_group_ids: [] };
}
+17
View File
@@ -245,3 +245,20 @@ export function useFederatedConnector(
error,
};
}
interface ConnectorGroupRestrictionsStatus {
enabled: boolean;
}
/**
* Whether connector forms offer the data-access group restriction. Set by the
* workspace toggle in Security and Hardening. Fails closed: hidden until the
* setting loads, and hidden if the request fails.
*/
export function useConnectorGroupRestrictionsEnabled(): boolean {
const { data } = useSWR<ConnectorGroupRestrictionsStatus>(
SWR_KEYS.connectorGroupRestrictions,
errorHandlingFetcher
);
return data?.enabled ?? false;
}
+6 -1
View File
@@ -1,5 +1,6 @@
import * as Yup from "yup";
import type { AccessTypeGroupSelectorFormType } from "@/components/admin/connectors/AccessTypeGroupSelector";
import type { ConnectorGroupRestrictionFormValues } from "@/lib/connectors/accessType";
import type {
ConfigurableSources,
IndexAttemptStage,
@@ -60,13 +61,17 @@ const buildInitialValuesForFields = (
export function createConnectorInitialValues(
connector: ConfigurableSources
): Record<string, any> & AccessTypeGroupSelectorFormType {
): Record<string, any> &
AccessTypeGroupSelectorFormType &
ConnectorGroupRestrictionFormValues {
const configuration = connectorConfigs[connector];
return {
name: "",
groups: [],
access_type: "public",
restrict_access_to_groups: false,
restriction_group_ids: [],
...buildInitialValuesForFields(configuration.values),
...buildInitialValuesForFields(configuration.advanced_values),
};
+1
View File
@@ -71,6 +71,7 @@ export const SWR_KEYS = {
indexingStatus: "/api/manage/admin/connector/indexing-status",
adminConnectorStatus: "/api/manage/admin/connector/status",
federatedConnectors: "/api/federated",
connectorGroupRestrictions: "/api/manage/connector-group-restrictions",
// ── Google Connectors ─────────────────────────────────────────────────────
googleConnectorCredentials: (service: "gmail" | "google-drive") =>
+1 -1
View File
@@ -184,7 +184,7 @@ export type ValidStatuses =
| "not_started";
export type TaskStatus = "PENDING" | "STARTED" | "SUCCESS" | "FAILURE";
export type Feedback = "like" | "dislike" | "mixed";
export type AccessType = "public" | "private" | "sync";
export type AccessType = "public" | "private" | "sync" | "sync_restricted";
export type ProcessingMode = "REGULAR";
export type SessionType = "Chat" | "Search" | "Slack";
@@ -0,0 +1,116 @@
"use client";
import { useField } from "formik";
import { useTranslations } from "next-intl";
import {
Button,
Card,
EmptyMessageCard,
InputSingleComboBox,
InputSwitch,
} from "@opal/components";
import { ContentAction, InputHorizontal } from "@opal/layouts";
import { SvgUsers, SvgX } from "@opal/icons";
import { useUserGroups } from "@/lib/hooks";
import type { ConnectorGroupRestrictionFormValues } from "@/lib/connectors/accessType";
type FieldName = keyof ConnectorGroupRestrictionFormValues;
const RESTRICT_FIELD: FieldName = "restrict_access_to_groups";
const GROUP_IDS_FIELD: FieldName = "restriction_group_ids";
/**
* Data-access restriction for a perm-synced connector: only members of the
* chosen groups can read its documents, on top of the source's permissions.
* Separate from the management-access groups, which decide who can configure
* the connector.
*/
export function ConnectorGroupRestrictionPicker() {
const t = useTranslations("admin.connector.groupRestriction");
const { data: userGroups } = useUserGroups();
const [restrict, , restrictHelpers] = useField<boolean>(RESTRICT_FIELD);
const [groupIds, , groupIdsHelpers] = useField<number[]>(GROUP_IDS_FIELD);
const selectedIds = new Set(groupIds.value);
const selectedGroups = (userGroups ?? []).filter((group) =>
selectedIds.has(group.id)
);
const options = (userGroups ?? [])
.filter((group) => !selectedIds.has(group.id))
.map((group) => ({
value: String(group.id),
title: group.name,
description: t("memberCount", { count: group.users.length }),
}));
function addGroup(value: string) {
const id = Number(value);
if (!Number.isInteger(id) || selectedIds.has(id)) return;
void groupIdsHelpers.setValue([...groupIds.value, id]);
}
function removeGroup(id: number) {
void groupIdsHelpers.setValue(
groupIds.value.filter((groupId) => groupId !== id)
);
}
return (
<div className="flex w-full flex-col gap-3">
<InputHorizontal
title={t("toggle.title")}
description={t("toggle.description")}
withLabel
>
<InputSwitch
checked={restrict.value}
onCheckedChange={(checked) => void restrictHelpers.setValue(checked)}
/>
</InputHorizontal>
{restrict.value && (
<>
<InputSingleComboBox
value=""
onValueChange={addGroup}
options={options}
placeholder={t("picker.placeholder")}
/>
{selectedGroups.length === 0 ? (
<EmptyMessageCard
sizePreset="main-ui"
icon={SvgUsers}
title={t("empty.title")}
description={t("empty.description")}
/>
) : (
<div className="grid max-h-80 grid-cols-1 gap-2 overflow-y-auto sm:grid-cols-2">
{selectedGroups.map((group) => (
<Card key={group.id} color="background-tint-01" padding={2}>
<ContentAction
icon={SvgUsers}
title={group.name}
description={t("memberCount", {
count: group.users.length,
})}
sizePreset="main-content"
variant="section"
rightChildren={
<Button
icon={SvgX}
prominence="tertiary"
tooltip={t("remove.tooltip", { name: group.name })}
onClick={() => removeGroup(group.id)}
/>
}
/>
</Card>
))}
</div>
)}
</>
)}
</div>
);
}
@@ -66,6 +66,7 @@ import { deleteConnector } from "@/lib/connector";
import ConnectorDocsLink from "@/components/admin/connectors/ConnectorDocsLink";
import { SvgArrowExchange, SvgKey, SvgSimpleLoader } from "@opal/icons";
import { useTranslations } from "next-intl";
import { toWireAccess } from "@/lib/connectors/accessType";
export interface AdvancedConfig {
refreshFreq: number;
@@ -334,7 +335,9 @@ export default function AddConnector({
const {
name,
groups,
access_type,
access_type: formAccessType,
restrict_access_to_groups,
restriction_group_ids,
pruneFreq,
indexingStart,
refreshFreq,
@@ -342,6 +345,12 @@ export default function AddConnector({
...connector_specific_config
} = values;
const wireAccess = toWireAccess(formAccessType, {
restrict_access_to_groups,
restriction_group_ids,
});
const access_type = wireAccess.access_type;
// Apply special transforms according to application logic
const transformedConnectorSpecificConfig = Object.entries(
connector_specific_config
@@ -473,6 +482,8 @@ export default function AddConnector({
currentCredential ||
liveGDriveCredential ||
liveGmailCredential;
// TODO(evan, ENG-4342): send wireAccess.restriction_group_ids
// once the backend accepts them; this call creates the cc-pair.
const linkCredentialResponse = await linkCredential(
response.id,
credential!.id,
@@ -69,6 +69,7 @@ import { useUser } from "@/providers/UserProvider";
import { resolveAllErrorsForCCPair } from "@/lib/targeted_reindex";
import { SWR_KEYS } from "@/lib/swr-keys";
import { can } from "@/lib/permissions/resource-actions";
import { isPermSynced } from "@/lib/connectors/accessType";
// synchronize these validations with the SQLAlchemy connector class until we have a
// centralized schema for both frontend and backend
const RefreshFrequencySchema = Yup.object().shape({
@@ -666,7 +667,7 @@ function Main({ ccPairId }: { ccPairId: number }) {
</div>
</div>
{ccPair.access_type === "sync" && (
{isPermSynced(ccPair.access_type) && (
<>
<div className="w-[200px]">
{/* TODO: Remove className and switch to text03 once Text is fully integrated across this page */}
@@ -783,7 +784,7 @@ function Main({ ccPairId }: { ccPairId: number }) {
)}
{indexAttempts &&
(ccPair.access_type === "sync" ? (
(isPermSynced(ccPair.access_type) ? (
<Section height="auto" alignItems="stretch" className="mt-6">
<SyncAttemptsTabs
ccPair={ccPair}
@@ -38,6 +38,7 @@ import { ConnectorStaggeredSkeleton } from "./ConnectorRowSkeleton";
import { Button } from "@opal/components";
import { SvgSettings } from "@opal/icons";
import { can } from "@/lib/permissions/resource-actions";
import { isPermSynced } from "@/lib/connectors/accessType";
// Helper to handle navigation with cmd/ctrl+click support
// NOTE: using this rather than Next/Link (or similar) since shadcn
@@ -202,7 +203,7 @@ function ConnectorRow({
<Badge variant={isEditable ? "success" : "default"} icon={FiUnlock}>
{t("status.access.organizationPublic.label")}
</Badge>
) : ccPairsIndexingStatus.access_type === "sync" ? (
) : isPermSynced(ccPairsIndexingStatus.access_type) ? (
<Badge
variant={isEditable ? "auto-sync" : "default"}
icon={FiRefreshCw}