FEAT - LAUNCHER: модульный доступ Codex Agent API

This commit is contained in:
DCCONSTRUCTIONS
2026-05-14 20:32:26 +03:00
parent 5e18a7f3c2
commit 34917e007a
8 changed files with 463 additions and 15 deletions
+28
View File
@@ -28,6 +28,7 @@ import {
rejectAdminTaskerInviteRequest,
removeAdminTaskManagerProjectMembership,
removeAdminTaskManagerWorkspaceMembership,
setAdminServiceModuleEntitlement,
setAdminUserServiceAccess,
updateAdminClient,
updateAdminAccessRequest,
@@ -68,6 +69,7 @@ import {
type AccessAssignmentValue,
type CreateUserCommand,
type EnsureTaskManagerProjectMemberCommand,
type SetServiceModuleEntitlementCommand,
type SetUserServiceAccessCommand,
} from "../widgets/admin-overlay/AdminOverlay";
import { ProfileSettingsPanel } from "../widgets/profile-settings-panel/ProfileSettingsPanel";
@@ -101,6 +103,7 @@ export function LauncherApp() {
const [authApps, setAuthApps] = useState<LauncherAuthApp[] | null>(null);
const [profileSettingsOpen, setProfileSettingsOpen] = useState(false);
const [pendingAccessAssignments, setPendingAccessAssignments] = useState<Record<string, AccessAssignmentValue>>({});
const [pendingServiceModuleEntitlements, setPendingServiceModuleEntitlements] = useState<Record<string, boolean>>({});
const [pendingTaskManagerMemberships, setPendingTaskManagerMemberships] = useState<Record<string, boolean>>({});
const [pendingTaskManagerProjectMemberships, setPendingTaskManagerProjectMemberships] = useState<Record<string, boolean>>({});
const [taskManagerWorkspaces, setTaskManagerWorkspaces] = useState<TaskManagerWorkspaceSummary[]>([]);
@@ -552,6 +555,29 @@ export function LauncherApp() {
});
}
function handleSetServiceModuleEntitlement(command: SetServiceModuleEntitlementCommand) {
const entitlementKey = `${command.clientId}:${command.userId}:${command.serviceId}:${command.moduleId}`;
if (pendingServiceModuleEntitlements[entitlementKey]) {
return;
}
setPendingServiceModuleEntitlements((current) => ({ ...current, [entitlementKey]: true }));
setAdminServiceModuleEntitlement(command)
.then((result) => {
setData(syncLauncherServiceLinks(result.data));
})
.catch((error: unknown) => {
console.warn(error instanceof Error ? error.message : "Не удалось обновить модуль сервиса");
})
.finally(() => {
setPendingServiceModuleEntitlements((current) => {
const { [entitlementKey]: _completed, ...rest } = current;
return rest;
});
});
}
function handleSetTaskManagerWorkspaceMemberRole(command: { clientId: string; userId: string; role: TaskManagerWorkspaceMemberRole; workspaceSlug?: string }) {
const membershipKey = `${command.clientId}:${command.userId}:${command.workspaceSlug ?? "primary"}`;
@@ -922,6 +948,7 @@ export function LauncherApp() {
onUpdateMembership={handleUpdateMembership}
onDeleteMembership={handleDeleteMembership}
pendingAccessAssignments={pendingAccessAssignments}
pendingServiceModuleEntitlements={pendingServiceModuleEntitlements}
onCreateGroup={handleCreateGroup}
onUpdateGroup={handleUpdateGroup}
onDeleteGroup={handleDeleteGroup}
@@ -938,6 +965,7 @@ export function LauncherApp() {
onRefreshTaskManagerWorkspaces={() => void refreshTaskManagerWorkspaces()}
onSetTaskManagerWorkspaceMemberRole={handleSetTaskManagerWorkspaceMemberRole}
onSetTaskManagerProjectMemberRole={handleSetTaskManagerProjectMemberRole}
onSetServiceModuleEntitlement={handleSetServiceModuleEntitlement}
/>
) : null}
{profileSettingsOpen && activeProfileUser ? (
+14
View File
@@ -36,3 +36,17 @@ export interface EffectiveAccessResult {
source?: ServiceGrantTargetType | "exception";
sourceId?: string;
}
export type ServiceModuleId = "codex_agents";
export interface ServiceModuleEntitlement {
id: string;
clientId: string;
serviceId: string;
userId: string;
moduleId: ServiceModuleId;
enabled: boolean;
createdByUserId?: string | null;
createdAt: string;
updatedAt: string;
}
+14 -1
View File
@@ -1,5 +1,5 @@
import type { AccessRequest } from "../../entities/access-request/types";
import type { ServiceAccessException, ServiceAppRole, ServiceGrant } from "../../entities/access/types";
import type { ServiceAccessException, ServiceAppRole, ServiceGrant, ServiceModuleEntitlement, ServiceModuleId } from "../../entities/access/types";
import type { Client, TaskManagerWorkspaceManagedBy } from "../../entities/client/types";
import type { Invite } from "../../entities/invite/types";
import type { Service } from "../../entities/service/types";
@@ -404,6 +404,19 @@ export async function setAdminUserServiceAccess(payload: {
});
}
export async function setAdminServiceModuleEntitlement(payload: {
clientId: string;
userId: string;
serviceId: string;
moduleId: ServiceModuleId;
enabled: boolean;
}): Promise<ControlPlaneMutationResult & { entitlement?: ServiceModuleEntitlement | null }> {
return requestJson<ControlPlaneMutationResult & { entitlement?: ServiceModuleEntitlement | null }>("/api/admin/access/service-modules", {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function upsertAdminGrant(payload: Partial<ServiceGrant>): Promise<ControlPlaneMutationResult> {
return requestJson<ControlPlaneMutationResult>("/api/admin/access/grants", {
method: "POST",
+3 -1
View File
@@ -1,6 +1,6 @@
import { computeEffectiveAccess } from "../../entities/access/computeEffectiveAccess";
import type { AccessRequest } from "../../entities/access-request/types";
import type { EffectiveAccessResult, ServiceAccessException, ServiceGrant } from "../../entities/access/types";
import type { EffectiveAccessResult, ServiceAccessException, ServiceGrant, ServiceModuleEntitlement } from "../../entities/access/types";
import type { Client, TaskManagerWorkspaceManagedBy } from "../../entities/client/types";
import type { Invite } from "../../entities/invite/types";
import { PUBLIC_POOL_CLIENT, isPublicPoolClientId } from "../../entities/public-pool/constants";
@@ -62,6 +62,7 @@ export interface LauncherData {
services: Service[];
grants: ServiceGrant[];
exceptions: ServiceAccessException[];
serviceModuleEntitlements: ServiceModuleEntitlement[];
invites: Invite[];
accessRequests: AccessRequest[];
revokedAccounts: RevokedAccount[];
@@ -214,6 +215,7 @@ export function normalizeLauncherData(data: Partial<LauncherData> | null | undef
services: Array.isArray(payload.services) ? payload.services : mockServices,
grants: Array.isArray(payload.grants) ? payload.grants : mockGrants,
exceptions: Array.isArray(payload.exceptions) ? payload.exceptions : mockExceptions,
serviceModuleEntitlements: Array.isArray(payload.serviceModuleEntitlements) ? payload.serviceModuleEntitlements : [],
invites: Array.isArray(payload.invites) ? payload.invites : mockInvites,
accessRequests: Array.isArray(payload.accessRequests) ? payload.accessRequests : mockAccessRequests,
revokedAccounts: Array.isArray(payload.revokedAccounts) ? payload.revokedAccounts : [],
+114
View File
@@ -3841,6 +3841,120 @@ code {
font-size: 0.72rem;
}
.task-module-access-card {
gap: 0.65rem;
}
.task-module-access-card__head {
display: grid;
gap: 0.2rem;
}
.task-module-access-card__head strong {
color: var(--text-secondary);
font-size: 0.86rem;
}
.task-module-access-card__head small {
color: var(--text-muted);
font-size: 0.74rem;
line-height: 1.35;
}
.task-module-access-list {
display: grid;
gap: 0.55rem;
}
.task-module-access-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.85rem;
align-items: center;
width: 100%;
min-height: 4rem;
border: 0;
border-radius: 0.95rem;
background: rgba(255, 255, 255, 0.045);
color: var(--text-secondary);
padding: 0.72rem;
text-align: left;
cursor: pointer;
transition:
background 160ms ease,
opacity 160ms ease,
transform 160ms ease;
}
.task-module-access-row:hover:not(:disabled),
.task-module-access-row:focus-visible:not(:disabled) {
background: rgba(255, 255, 255, 0.075);
transform: translateY(-1px);
}
.task-module-access-row--enabled {
background: rgba(181, 255, 90, 0.1);
}
.task-module-access-row--pending {
cursor: progress;
opacity: 0.58;
}
.task-module-access-row__meta {
display: grid;
min-width: 0;
gap: 0.18rem;
}
.task-module-access-row__meta strong,
.task-module-access-row__meta small {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.task-module-access-row__meta strong {
color: var(--text-primary);
font-size: 0.84rem;
}
.task-module-access-row__meta small {
color: var(--text-muted);
font-size: 0.73rem;
line-height: 1.35;
}
.task-module-access-row__state {
display: inline-flex;
align-items: center;
gap: 0.48rem;
color: var(--text-muted);
font-size: 0.72rem;
font-weight: 760;
white-space: nowrap;
}
.task-module-checker {
display: grid;
width: 1.1rem;
height: 1.1rem;
place-items: center;
border-radius: 999px;
background: rgba(255, 255, 255, 0.1);
}
.task-module-access-row--enabled .task-module-checker {
background: rgb(var(--nodedc-accent-rgb));
}
.task-module-checker span {
width: 0.38rem;
height: 0.38rem;
border-radius: 999px;
background: rgb(var(--nodedc-on-accent-rgb));
}
.access-explanation {
display: grid;
align-content: start;
+173 -13
View File
@@ -41,7 +41,7 @@ import {
X,
} from "lucide-react";
import type { AccessRequest, AccessRequestStatus } from "../../entities/access-request/types";
import type { ServiceAppRole } from "../../entities/access/types";
import type { ServiceAppRole, ServiceModuleId } from "../../entities/access/types";
import type { Client, ClientStatus, ClientTaskManagerWorkspaceBinding, ClientType } from "../../entities/client/types";
import type { Invite, InviteStatus } from "../../entities/invite/types";
import { PUBLIC_POOL_CLIENT_ID, PUBLIC_POOL_CONTEXT_DESCRIPTION, PUBLIC_POOL_CONTEXT_LABEL, isPublicPoolClientId } from "../../entities/public-pool/constants";
@@ -102,6 +102,14 @@ export interface SetUserServiceAccessCommand {
value: AccessAssignmentValue;
}
export interface SetServiceModuleEntitlementCommand {
clientId: string;
userId: string;
serviceId: string;
moduleId: ServiceModuleId;
enabled: boolean;
}
export interface CreateUserCommand {
clientId: string;
email: string;
@@ -180,6 +188,7 @@ export function AdminOverlay({
onUpdateMembership,
onDeleteMembership,
pendingAccessAssignments,
pendingServiceModuleEntitlements,
onCreateGroup,
onUpdateGroup,
onDeleteGroup,
@@ -196,6 +205,7 @@ export function AdminOverlay({
onRefreshTaskManagerWorkspaces,
onSetTaskManagerWorkspaceMemberRole,
onSetTaskManagerProjectMemberRole,
onSetServiceModuleEntitlement,
}: {
data: LauncherData;
me: MeResponse;
@@ -227,6 +237,7 @@ export function AdminOverlay({
onUpdateMembership: (membershipId: string, patch: Partial<ClientMembership>) => void;
onDeleteMembership: (membershipId: string) => void;
pendingAccessAssignments: Record<string, AccessAssignmentValue>;
pendingServiceModuleEntitlements: Record<string, boolean>;
onCreateGroup: (clientId: string) => void;
onUpdateGroup: (groupId: string, patch: Partial<ClientGroup>) => void;
onDeleteGroup: (groupId: string) => void;
@@ -243,6 +254,7 @@ export function AdminOverlay({
onRefreshTaskManagerWorkspaces: () => void;
onSetTaskManagerWorkspaceMemberRole: (command: EnsureTaskManagerWorkspaceMemberCommand) => void;
onSetTaskManagerProjectMemberRole: (command: EnsureTaskManagerProjectMemberCommand) => void;
onSetServiceModuleEntitlement: (command: SetServiceModuleEntitlementCommand) => void;
}) {
const isRoot = me.launcherRole === "root_admin";
const isPlatformMode = isRoot && mode === "platform";
@@ -510,6 +522,7 @@ export function AdminOverlay({
onSelectCell={(cell) => setSelectedCell({ userId: cell.userId, serviceId: cell.serviceId })}
onSetUserServiceAccess={onSetUserServiceAccess}
pendingAccessAssignments={pendingAccessAssignments}
pendingServiceModuleEntitlements={pendingServiceModuleEntitlements}
onUpdateUser={onUpdateUser}
onUpdateMembership={onUpdateMembership}
pendingTaskManagerMemberships={pendingTaskManagerMemberships}
@@ -517,6 +530,7 @@ export function AdminOverlay({
taskManagerWorkspaceCatalog={taskManagerWorkspaces}
onSetTaskManagerWorkspaceMemberRole={onSetTaskManagerWorkspaceMemberRole}
onSetTaskManagerProjectMemberRole={onSetTaskManagerProjectMemberRole}
onSetServiceModuleEntitlement={onSetServiceModuleEntitlement}
/>
) : null}
{activeSection === "invites" ? (
@@ -1367,6 +1381,22 @@ const taskManagerProjectRoleOptions: Array<NodeDcSelectOption<OperationalCoreRol
{ value: "pending", label: "Сохраняем...", disabled: true, hidden: true },
];
const operationalCoreModules: Array<{
id: ServiceModuleId;
title: string;
description: string;
enabledLabel: string;
disabledLabel: string;
}> = [
{
id: "codex_agents",
title: "Codex Agent API",
description: "Разрешает пользователю подключать локального Codex-агента к своим разрешённым workspace/project в Operational Core.",
enabledLabel: "Модуль включён",
disabledLabel: "Модуль выключен",
},
];
function membershipRoleLabel(role: ClientMembershipRole): string {
return membershipRoleOptions.find((option) => option.value === role)?.label ?? role;
}
@@ -2874,6 +2904,7 @@ function AccessSection({
onSelectCell,
onSetUserServiceAccess,
pendingAccessAssignments,
pendingServiceModuleEntitlements,
onUpdateUser,
onUpdateMembership,
pendingTaskManagerMemberships,
@@ -2881,6 +2912,7 @@ function AccessSection({
taskManagerWorkspaceCatalog,
onSetTaskManagerWorkspaceMemberRole,
onSetTaskManagerProjectMemberRole,
onSetServiceModuleEntitlement,
}: {
data: LauncherData;
matrix: ReturnType<typeof buildAccessMatrix>;
@@ -2888,6 +2920,7 @@ function AccessSection({
onSelectCell: (cell: AccessMatrixCell) => void;
onSetUserServiceAccess: (command: SetUserServiceAccessCommand) => void;
pendingAccessAssignments: Record<string, AccessAssignmentValue>;
pendingServiceModuleEntitlements: Record<string, boolean>;
onUpdateUser: (userId: string, patch: Partial<LauncherUser>) => void;
onUpdateMembership: (membershipId: string, patch: Partial<ClientMembership>) => void;
pendingTaskManagerMemberships: Record<string, boolean>;
@@ -2895,6 +2928,7 @@ function AccessSection({
taskManagerWorkspaceCatalog: TaskManagerWorkspaceSummary[];
onSetTaskManagerWorkspaceMemberRole: (command: EnsureTaskManagerWorkspaceMemberCommand) => void;
onSetTaskManagerProjectMemberRole: (command: EnsureTaskManagerProjectMemberCommand) => void;
onSetServiceModuleEntitlement: (command: SetServiceModuleEntitlementCommand) => void;
}) {
const hasUsers = matrix.users.length > 0;
const isPublicPoolContext = isPublicPoolClientId(matrix.client.id);
@@ -3010,7 +3044,7 @@ function AccessSection({
role: accessAssignmentToTaskManagerRole(nextValue),
});
}}
onOpenDetails={isTaskManagerService && !usePublicTaskerAccess ? () => setDetailsCell(cell) : undefined}
onOpenDetails={isTaskManagerService ? () => setDetailsCell(cell) : undefined}
/>
</div>
);
@@ -3029,15 +3063,18 @@ function AccessSection({
user={getUser(data, detailsCell.userId)}
service={detailsService}
cell={detailsCell}
workspaces={clientTaskManagerWorkspaces}
workspaces={isPublicPoolContext ? [] : clientTaskManagerWorkspaces}
workspaceCatalog={taskManagerWorkspaceCatalog}
publicSelfService={isPublicPoolContext}
pendingAccessAssignments={pendingAccessAssignments}
pendingServiceModuleEntitlements={pendingServiceModuleEntitlements}
pendingTaskManagerMemberships={pendingTaskManagerMemberships}
pendingTaskManagerProjectMemberships={pendingTaskManagerProjectMemberships}
onClose={() => setDetailsCell(null)}
onSetUserServiceAccess={onSetUserServiceAccess}
onSetTaskManagerWorkspaceMemberRole={onSetTaskManagerWorkspaceMemberRole}
onSetTaskManagerProjectMemberRole={onSetTaskManagerProjectMemberRole}
onSetServiceModuleEntitlement={onSetServiceModuleEntitlement}
/>
) : null}
</div>
@@ -3051,8 +3088,10 @@ function PublicAccessUsersPanel({
onSelectCell,
onSetUserServiceAccess,
pendingAccessAssignments,
pendingServiceModuleEntitlements,
onUpdateUser,
onUpdateMembership,
onSetServiceModuleEntitlement,
}: {
data: LauncherData;
matrix: ReturnType<typeof buildAccessMatrix>;
@@ -3060,10 +3099,14 @@ function PublicAccessUsersPanel({
onSelectCell: (cell: AccessMatrixCell) => void;
onSetUserServiceAccess: (command: SetUserServiceAccessCommand) => void;
pendingAccessAssignments: Record<string, AccessAssignmentValue>;
pendingServiceModuleEntitlements: Record<string, boolean>;
onUpdateUser: (userId: string, patch: Partial<LauncherUser>) => void;
onUpdateMembership: (membershipId: string, patch: Partial<ClientMembership>) => void;
onSetServiceModuleEntitlement: (command: SetServiceModuleEntitlementCommand) => void;
}) {
const operationalCoreService = matrix.services.find(isOperationalCoreService) ?? null;
const [detailsCell, setDetailsCell] = useState<AccessMatrixCell | null>(null);
const detailsService = detailsCell ? getService(data, detailsCell.serviceId) : null;
return (
<GlassSurface className="table-shell table-shell--users table-shell--public-access-users">
@@ -3168,13 +3211,8 @@ function PublicAccessUsersPanel({
pendingValue={pendingAccessAssignments[accessCellKey(user.id, operationalCoreCell.serviceId)]}
publicSelfService
onSelectCell={onSelectCell}
onSetAccess={(value) =>
onSetUserServiceAccess({
userId: user.id,
serviceId: operationalCoreCell.serviceId,
value,
})
}
onSetAccess={() => undefined}
onOpenDetails={() => setDetailsCell(operationalCoreCell)}
/>
) : (
<span className="muted-text"></span>
@@ -3185,6 +3223,27 @@ function PublicAccessUsersPanel({
})}
</tbody>
</table>
{detailsCell && detailsService ? (
<OperationalCoreAccessModal
data={data}
client={matrix.client}
user={getUser(data, detailsCell.userId)}
service={detailsService}
cell={detailsCell}
workspaces={[]}
workspaceCatalog={[]}
publicSelfService
pendingAccessAssignments={pendingAccessAssignments}
pendingServiceModuleEntitlements={pendingServiceModuleEntitlements}
pendingTaskManagerMemberships={{}}
pendingTaskManagerProjectMemberships={{}}
onClose={() => setDetailsCell(null)}
onSetUserServiceAccess={onSetUserServiceAccess}
onSetTaskManagerWorkspaceMemberRole={() => undefined}
onSetTaskManagerProjectMemberRole={() => undefined}
onSetServiceModuleEntitlement={onSetServiceModuleEntitlement}
/>
) : null}
</GlassSurface>
);
}
@@ -3280,13 +3339,16 @@ function OperationalCoreAccessModal({
cell,
workspaces,
workspaceCatalog,
publicSelfService = false,
pendingAccessAssignments,
pendingTaskManagerMemberships,
pendingTaskManagerProjectMemberships,
pendingServiceModuleEntitlements,
onClose,
onSetUserServiceAccess,
onSetTaskManagerWorkspaceMemberRole,
onSetTaskManagerProjectMemberRole,
onSetServiceModuleEntitlement,
}: {
data: LauncherData;
client: Client;
@@ -3295,18 +3357,24 @@ function OperationalCoreAccessModal({
cell: AccessMatrixCell;
workspaces: ClientTaskManagerWorkspaceBinding[];
workspaceCatalog: TaskManagerWorkspaceSummary[];
publicSelfService?: boolean;
pendingAccessAssignments: Record<string, AccessAssignmentValue>;
pendingTaskManagerMemberships: Record<string, boolean>;
pendingTaskManagerProjectMemberships: Record<string, boolean>;
pendingServiceModuleEntitlements: Record<string, boolean>;
onClose: () => void;
onSetUserServiceAccess: (command: SetUserServiceAccessCommand) => void;
onSetTaskManagerWorkspaceMemberRole: (command: EnsureTaskManagerWorkspaceMemberCommand) => void;
onSetTaskManagerProjectMemberRole: (command: EnsureTaskManagerProjectMemberCommand) => void;
onSetServiceModuleEntitlement: (command: SetServiceModuleEntitlementCommand) => void;
}) {
const membership = data.memberships.find((item) => item.clientId === client.id && item.userId === user.id);
const protectedUser = user.id === "user_root" || membership?.role === "client_owner";
const basePendingValue = pendingAccessAssignments[accessCellKey(user.id, service.id)];
const baseAssignmentValue = basePendingValue ?? accessAssignmentValue(cell);
const baseSelectOptions = publicSelfService ? publicOperationalCoreAccessOptions : accessAssignmentOptions;
const baseSelectValue = publicSelfService ? publicOperationalCoreSelectValue(baseAssignmentValue) : baseAssignmentValue;
const basePending = basePendingValue !== undefined;
return (
<div className="service-content-modal-layer" role="dialog" aria-modal="true" aria-label={`Operational Core доступы ${user.name}`}>
@@ -3319,7 +3387,82 @@ function OperationalCoreAccessModal({
</div>
<div className="task-workspace-access-list">
{workspaces.length ? (
<section className="task-workspace-access-card task-access-base-card">
<div className="task-workspace-access-card__head">
<div>
<strong>Базовый доступ</strong>
<small>
{publicSelfService
? "Открытый контур: workspace member, service admin или блокировка instance."
: "Глобальная роль пользователя в Operational Core для выбранного клиента."}
</small>
</div>
{protectedUser ? (
<AdminStaticPill>{accessAssignmentLabel("admin")}</AdminStaticPill>
) : (
<NodeDcSelect
className="admin-table-select-wrap"
triggerClassName="admin-table-select-trigger"
value={baseSelectValue}
options={baseSelectOptions}
label={`Базовый доступ ${user.name} к Operational Core`}
minMenuWidth={220}
disabled={basePending}
onChange={(nextValue) => {
onSetUserServiceAccess({ userId: user.id, serviceId: service.id, value: nextValue });
}}
/>
)}
</div>
</section>
<section className="task-workspace-access-card task-module-access-card">
<div className="task-module-access-card__head">
<div>
<strong>Дополнительные модули ops-слоя</strong>
<small>Модули работают только внутри выданного доступа к Operational Core и не расширяют workspace/project права сами по себе.</small>
</div>
</div>
<div className="task-module-access-list">
{operationalCoreModules.map((module) => {
const enabled = hasServiceModuleEntitlement(data, client.id, user.id, service.id, module.id);
const pendingKey = serviceModuleEntitlementKey(client.id, user.id, service.id, module.id);
const pending = Boolean(pendingServiceModuleEntitlements[pendingKey]);
return (
<button
key={module.id}
className={cn("task-module-access-row", enabled && "task-module-access-row--enabled", pending && "task-module-access-row--pending")}
type="button"
aria-pressed={enabled}
disabled={pending}
onClick={() =>
onSetServiceModuleEntitlement({
clientId: client.id,
userId: user.id,
serviceId: service.id,
moduleId: module.id,
enabled: !enabled,
})
}
>
<span className="task-module-access-row__meta">
<strong>{module.title}</strong>
<small>{module.description}</small>
</span>
<span className="task-module-access-row__state">
<span className="task-module-checker" aria-hidden="true">
{enabled ? <span /> : null}
</span>
<span>{pending ? "Сохраняем..." : enabled ? module.enabledLabel : module.disabledLabel}</span>
</span>
</button>
);
})}
</div>
</section>
{!publicSelfService ? (
workspaces.map((workspace) => {
const role = getTaskManagerMembershipRole(data, client.id, user.id, workspace.slug);
const projects = getWorkspaceCatalogProjects(workspace, workspaceCatalog);
@@ -3431,12 +3574,14 @@ function OperationalCoreAccessModal({
</section>
);
})
) : (
) : null}
{!publicSelfService && !workspaces.length ? (
<div className="access-empty-state">
<strong>Workspace не привязаны к клиенту</strong>
<span>Откройте карточку клиента и выберите Operational Core workspaces, после этого здесь появятся назначения.</span>
</div>
)}
) : null}
</div>
<div className="service-content-modal__foot">
@@ -4653,6 +4798,21 @@ function accessCellKey(userId: string, serviceId: string): string {
return `${userId}:${serviceId}`;
}
function serviceModuleEntitlementKey(clientId: string, userId: string, serviceId: string, moduleId: ServiceModuleId): string {
return `${clientId}:${userId}:${serviceId}:${moduleId}`;
}
function hasServiceModuleEntitlement(data: LauncherData, clientId: string, userId: string, serviceId: string, moduleId: ServiceModuleId): boolean {
return data.serviceModuleEntitlements.some(
(entitlement) =>
entitlement.clientId === clientId &&
entitlement.userId === userId &&
entitlement.serviceId === serviceId &&
entitlement.moduleId === moduleId &&
entitlement.enabled
);
}
function isOperationalCoreService(service: Service): boolean {
return service.slug === "task-manager" || service.authentikApplicationSlug === "task-manager";
}