diff --git a/src/i18n-keysets/component.iam-access-dialog/en.json b/src/i18n-keysets/component.iam-access-dialog/en.json index 18fdc67f36..1b187c7570 100644 --- a/src/i18n-keysets/component.iam-access-dialog/en.json +++ b/src/i18n-keysets/component.iam-access-dialog/en.json @@ -1,4 +1,5 @@ { + "action_add": "Add", "action_add-user": "Add user", "action_cancel": "Cancel", "action_choose-user": "Select user", @@ -7,6 +8,7 @@ "action_revoke-access": "Revoke role", "action_save": "Save", "button_bindings-list-retry": "Reload list", + "label_access": "Access rights", "label_access_error": "Insufficient rights", "label_all": "All", "label_groups": "Groups", @@ -21,6 +23,7 @@ "role_admin": "Admin", "role_editor": "Editor", "role_limited-viewer": "Limited viewer", + "role_limited-viewer-hint": "Unlike 'Viewer', it doesn't grant access to view datasets and connections", "role_viewer": "Viewer", "section_direct_accesses": "Direct permissions", "section_inherited_accesses": "Inherited permissions", diff --git a/src/i18n-keysets/component.iam-access-dialog/ru.json b/src/i18n-keysets/component.iam-access-dialog/ru.json index 51f53c243a..317de52ef8 100644 --- a/src/i18n-keysets/component.iam-access-dialog/ru.json +++ b/src/i18n-keysets/component.iam-access-dialog/ru.json @@ -1,4 +1,5 @@ { + "action_add": "Добавить", "action_add-user": "Добавить пользователя", "action_cancel": "Отменить", "action_choose-user": "Выбрать пользователя", @@ -7,6 +8,7 @@ "action_revoke-access": "Отозвать роль", "action_save": "Сохранить", "button_bindings-list-retry": "Обновить список", + "label_access": "Права доступа", "label_access_error": "Недостаточно прав", "label_all": "Все", "label_groups": "Группы", @@ -21,6 +23,7 @@ "role_admin": "Администрирование", "role_editor": "Редактирование", "role_limited-viewer": "Ограниченный просмотр", + "role_limited-viewer-hint": "В отличие от просмотра, не даёт доступ просмотру датасетов и подключений", "role_viewer": "Просмотр", "section_direct_accesses": "Прямые права", "section_inherited_accesses": "Наследуемые права", diff --git a/src/server/components/features/features-list/EnableNewAccessDialog.ts b/src/server/components/features/features-list/EnableNewAccessDialog.ts new file mode 100644 index 0000000000..a70acf030f --- /dev/null +++ b/src/server/components/features/features-list/EnableNewAccessDialog.ts @@ -0,0 +1,10 @@ +import {Feature} from '../../../../shared'; +import {createFeatureConfig} from '../utils'; + +export default createFeatureConfig({ + name: Feature.EnableNewAccessDialog, + state: { + development: false, + production: false, + }, +}); diff --git a/src/shared/schema/extensions/actions/iam-access-dialog.ts b/src/shared/schema/extensions/actions/iam-access-dialog.ts index 71c79acfe4..1f66936bed 100644 --- a/src/shared/schema/extensions/actions/iam-access-dialog.ts +++ b/src/shared/schema/extensions/actions/iam-access-dialog.ts @@ -1,6 +1,8 @@ import {createAction} from '../../gateway-utils'; import type {GetDatalensOperationResponse} from '../../us/types/operations'; import type { + BatchListAccessBindingsArgs, + BatchListAccessBindingsResponse, BatchListMembersArgs, BatchListMembersResponse, GetClaimsArgs, @@ -40,4 +42,13 @@ export const iamAccessDialogActions = { batchListMembers: createAction(async () => { return {members: [], nextPageToken: ''}; }), + batchListAccessBindings: createAction< + BatchListAccessBindingsResponse, + BatchListAccessBindingsArgs + >(async () => { + return { + subjectsWithBindings: [], + nextPageToken: '', + }; + }), }; diff --git a/src/shared/schema/extensions/types/iam-access-dialog.ts b/src/shared/schema/extensions/types/iam-access-dialog.ts index da8512202b..0aa23fdf0e 100644 --- a/src/shared/schema/extensions/types/iam-access-dialog.ts +++ b/src/shared/schema/extensions/types/iam-access-dialog.ts @@ -1,6 +1,7 @@ import type {Lang} from '../../..'; export enum AccessServiceResourceType { + Organization = 'organization-manager.organization', Collection = 'datalens.collection', Workbook = 'datalens.workbook', } @@ -20,6 +21,7 @@ export enum SubjectType { } export enum ClaimsSubjectType { + Unspecified = 'SUBJECT_TYPE_UNSPECIFIED', UserAccount = 'USER_ACCOUNT', Group = 'GROUP', Invitee = 'INVITEE', @@ -108,6 +110,7 @@ export interface SubjectClaims { pictureData?: string; picture?: string; idpType?: string | null; + displayName?: string; } export type SubjectDetails = { @@ -132,3 +135,41 @@ export type BatchListMembersResponse = { members: SubjectClaims[]; nextPageToken: string; }; + +export interface BatchListAccessBindingsResponse { + subjectsWithBindings: SubjectWithBindings[]; + nextPageToken: string; +} + +export interface SubjectWithBindings { + subjectClaims: SubjectClaims; + accessBindings: InheritedAccessBindings[]; + inheritedAccessBindings: InheritedAccessBindings[]; +} + +export interface InheritedAccessBindings { + roleId: string; + inheritedFrom: AccessBindingsResource | null; +} + +export interface AccessBindingsResource { + id: string; + type: string; +} + +export type BatchListAccessBindingsArgs = { + resourcePath: AccessBindingsResource[]; + getInheritedBindings?: boolean; + filter?: string; + pageSize?: number; + pageToken?: string; +}; + +export enum ResourceType { + Collection = 'collection', + Workbook = 'workbook', +} + +export type UpdateAccessBindingsRequest = { + accessBindingDeltas: AccessBindingDelta[]; +}; diff --git a/src/shared/schema/extensions/types/index.ts b/src/shared/schema/extensions/types/index.ts index 38b9d8be75..ac36c4e3c6 100644 --- a/src/shared/schema/extensions/types/index.ts +++ b/src/shared/schema/extensions/types/index.ts @@ -1 +1,2 @@ export * from './iam-access-dialog'; +export * from './invitations'; diff --git a/src/shared/schema/extensions/types/invitations.ts b/src/shared/schema/extensions/types/invitations.ts new file mode 100644 index 0000000000..e37dab95b8 --- /dev/null +++ b/src/shared/schema/extensions/types/invitations.ts @@ -0,0 +1,72 @@ +import {ResourceType} from './iam-access-dialog'; + +export type Invite = { + invitee: { + email: string; + }; +}; + +export type InviteUsersRequest = { + invitations: Invite[]; +}; + +export type InviteUsersWithAccessRequest = InviteUsersRequest & { + resourceType: ResourceType; + resourceId: string; + roleId: string; +}; + +export type InviteUsersResponse = { + validInvitations: InviteRespone[]; + invalidInvitations: InviteRespone[]; +}; + +export type InviteRespone = Invite & { + invitationId: string; + status: InviteStatus; + createdAt: string; + inviteeId: string; + subjectId: string; +}; + +export type DeleteInvitationRequest = { + invitationId: string; +}; + +export enum InviteStatus { + Creating = 'CREATING', + Pending = 'PENDING', + Accepted = 'ACCEPTED', + Rejected = 'REJECTED', +} + +export type ListOrganizationInvitationsRequest = { + status: InviteStatus; + filter?: string | undefined; + pageSize?: number | undefined; + pageToken?: string; +}; + +export type ListOrganizationInvitationsResponse = { + invitations: Invitation[]; + nextPageToken: string; +}; + +export type Invitation = Invite & { + id: string; + status: InviteStatus; + inviteeId: string; + subjectId: string; +}; + +export interface ResendInvitationRequest { + invitationId: string; + notAfter?: InvitationTimestamp; +} +export interface ResendInvitationResponse { + invitationId: string; + organizationId: string; + email: string; +} + +export type InvitationTimestamp = {seconds: string; nanos?: number}; diff --git a/src/shared/types/feature.ts b/src/shared/types/feature.ts index 41b4999c6f..f9ceea6a9e 100644 --- a/src/shared/types/feature.ts +++ b/src/shared/types/feature.ts @@ -98,12 +98,15 @@ export enum Feature { EnableSharedEntries = 'EnableSharedEntries', EnableMobileFixedHeader = 'EnableMobileFixedHeader', + /** enabled redesign/moving to drawers existing settings */ EnableCommonChartDashSettings = 'EnableCommonChartDashSettings', /** enables new dash & widgets settings */ EnableNewDashSettings = 'EnableNewDashSettings', /** Shows updated settings page */ EnableNewServiceSettings = 'EnableNewServiceSettings', + /** Enable new access dialog (AccessDialog) */ + EnableNewAccessDialog = 'EnableNewAccessDialog', } export type FeatureConfig = Record; diff --git a/src/ui/components/AccessDialog/index.tsx b/src/ui/components/AccessDialog/index.tsx new file mode 100644 index 0000000000..1313985fae --- /dev/null +++ b/src/ui/components/AccessDialog/index.tsx @@ -0,0 +1,8 @@ +import type {AccessDialogProps} from 'ui/registry/units/common/types/components/AccessDialog'; + +export const DIALOG_ACCESS = Symbol('DIALOG_ACCESS'); + +export type OpenDialogAccessDialogArgs = { + id: typeof DIALOG_ACCESS; + props: AccessDialogProps; +}; diff --git a/src/ui/components/EntryDialogues/DialogSwitchPublic/DialogPublic/AuthorSection/AuthorSection.tsx b/src/ui/components/EntryDialogues/DialogSwitchPublic/DialogPublic/AuthorSection/AuthorSection.tsx index 2eaaf0cf84..3fd8af3feb 100644 --- a/src/ui/components/EntryDialogues/DialogSwitchPublic/DialogPublic/AuthorSection/AuthorSection.tsx +++ b/src/ui/components/EntryDialogues/DialogSwitchPublic/DialogPublic/AuthorSection/AuthorSection.tsx @@ -12,7 +12,7 @@ import './AuthorSection.scss'; const b = block('dl-dialog-public-author-section'); const i18n = I18n.keyset('component.dialog-switch-public.view'); -type Props = { +export type AuthorSectionProps = { validationErrors: ValidationErrors; className?: string; scope: 'widget' | 'dash'; @@ -20,20 +20,38 @@ type Props = { progress: boolean; disabled: boolean; onChange: ({link, text}: {link?: string; text?: string}) => void; + showWarning?: boolean; + linkLabel?: string; + textLabel?: string; }; -function AuthorSection(props: Props) { - const {className, authorData, disabled, onChange, scope, validationErrors} = props; +function AuthorSection(props: AuthorSectionProps) { + const { + className, + authorData, + disabled, + onChange, + scope, + validationErrors, + showWarning = true, + linkLabel = i18n('label_author-link'), + textLabel = i18n('label_author-text'), + } = props; return (
-
{i18n('section_author')}
-
- {i18n('label_author-description', {subject: i18n(`label_author-subject-${scope}`)})} -
- + {showWarning && ( + +
{i18n('section_author')}
+
+ {i18n('label_author-description', { + subject: i18n(`label_author-subject-${scope}`), + })} +
+
+ )}
- + onChange({text: value})} > - + ( return isLoadingInherited && inheritedAccesses?.length === 0 ? ( ) : ( - <> + {footerInheritedAccessesTable} @@ -187,7 +187,7 @@ export const AccessList = React.memo( }} /> )} - + ); }; diff --git a/src/ui/registry/units/common/components-map.tsx b/src/ui/registry/units/common/components-map.tsx index 87c4c78ea6..e2b8a43cef 100644 --- a/src/ui/registry/units/common/components-map.tsx +++ b/src/ui/registry/units/common/components-map.tsx @@ -9,6 +9,7 @@ import {makeDefaultEmpty} from '../../components/DefaultEmpty'; import {Example} from './components/Example/Example'; import {EXAMPLE_COMPONENT} from './constants/components'; +import type {AccessDialogProps} from './types/components/AccessDialog'; import type {AccessRightsProps} from './types/components/AccessRights'; import type {AccessRightsUrlOpenProps} from './types/components/AccessRightsUrlOpen'; import type {AclSubjectProps} from './types/components/AclSubject'; @@ -66,4 +67,5 @@ export const commonComponentsMap = { WorkbookEntriesTableTabs: makeDefaultEmpty(), WorkbookEntryExtended: makeDefaultEmpty(), DialogEntryDescription: makeDefaultEmpty(), + AccessDialog: makeDefaultEmpty(), } as const; diff --git a/src/ui/registry/units/common/types/components/AccessDialog.ts b/src/ui/registry/units/common/types/components/AccessDialog.ts new file mode 100644 index 0000000000..7ed04ba880 --- /dev/null +++ b/src/ui/registry/units/common/types/components/AccessDialog.ts @@ -0,0 +1,9 @@ +export type AccessDialogProps = { + entryId?: string; + workbookId?: string; + collectionId?: string; + resourceTitle?: string; + canUpdateAccessBindings: boolean; + onClose?: () => void; + defaultTab?: string; +}; diff --git a/src/ui/registry/units/common/types/functions/useSubjectsListId.ts b/src/ui/registry/units/common/types/functions/useSubjectsListId.ts index 56de2fbc63..807bb4f009 100644 --- a/src/ui/registry/units/common/types/functions/useSubjectsListId.ts +++ b/src/ui/registry/units/common/types/functions/useSubjectsListId.ts @@ -1 +1,5 @@ -export type UseSubjectsListId = {type: 'organizationId' | 'cloudId'; id: string | undefined}; +export type UseSubjectsListId = { + type: 'organizationId' | 'cloudId'; + id: string | undefined; + title: string | undefined; +}; diff --git a/src/ui/store/actions/openDialogTypes.ts b/src/ui/store/actions/openDialogTypes.ts index 54035f7a80..23784cb423 100644 --- a/src/ui/store/actions/openDialogTypes.ts +++ b/src/ui/store/actions/openDialogTypes.ts @@ -60,6 +60,7 @@ import type {OpenDialogExportWorkbookArgs} from 'ui/components/CollectionsStruct import type {OpenDialogDefaultArgs} from 'ui/components/DialogDefault/DialogDefault'; import type {OpenDialogCreatePublicGalleryWorkbookArgs} from 'ui/components/CollectionsStructure/CreatePublicGalleryWorkbookDialog'; import type {OpenDialogEntryDescriptionArgs} from 'ui/components/DialogEntryDescription/DialogEntryDescriptionWrapper'; +import type {OpenDialogAccessDialogArgs} from 'ui/components/AccessDialog'; import type {OpenDialogSharedEntryBindingArgs} from 'ui/components/DialogSharedEntryBindings/DialogSharedEntryBindings'; import type {OpenDialogSharedEntryUnbindArgs} from 'ui/components/DialogSharedEntryUnbind/DialogSharedEntryUnbind'; import type {OpenDialogSharedEntryPermissionsArgs} from 'ui/components/DialogSharedEntryPermissions/DialogSharedEntryPermissions'; @@ -125,6 +126,8 @@ export type OpenDialogArgs = | OpenDialogExportWorkbookArgs | OpenDialogDefaultArgs | OpenDialogCreatePublicGalleryWorkbookArgs + | OpenDialogEntryDescriptionArgs + | OpenDialogAccessDialogArgs | OpenDialogSharedEntryBindingArgs | OpenDialogSharedEntryUnbindArgs | OpenDialogSharedEntryPermissionsArgs diff --git a/src/ui/units/collections/components/CollectionContent/hooks/useActions.tsx b/src/ui/units/collections/components/CollectionContent/hooks/useActions.tsx index 61dca9ee33..be30b75358 100644 --- a/src/ui/units/collections/components/CollectionContent/hooks/useActions.tsx +++ b/src/ui/units/collections/components/CollectionContent/hooks/useActions.tsx @@ -6,6 +6,7 @@ import {I18n} from 'i18n'; import {useDispatch} from 'react-redux'; import {useHistory} from 'react-router-dom'; import {WORKBOOK_STATUS} from 'shared/constants/workbooks'; +import {DIALOG_ACCESS} from 'ui/components/AccessDialog'; import {DIALOG_EXPORT_WORKBOOK} from 'ui/components/CollectionsStructure/ExportWorkbookDialog/ExportWorkbookDialog'; import {DIALOG_SHARED_ENTRY_BINDINGS} from 'ui/components/DialogSharedEntryBindings/DialogSharedEntryBindings'; import {isEnabledFeature} from 'ui/utils/isEnabledFeature'; @@ -49,6 +50,31 @@ type UseActionsArgs = { onCloseMoveDialog: (structureChanged: boolean) => void; }; +const openAccessDialog = ( + dispatch: AppDispatch, + params: { + collectionId?: string; + workbookId?: string; + resourceTitle?: string; + canUpdateAccessBindings?: boolean; + }, +) => { + dispatch( + openDialog({ + id: DIALOG_ACCESS, + props: { + collectionId: params.collectionId, + workbookId: params.workbookId, + resourceTitle: params.resourceTitle, + canUpdateAccessBindings: params.canUpdateAccessBindings ?? false, + onClose: () => { + dispatch(closeDialog()); + }, + }, + }), + ); +}; + export const useActions = ({fetchStructureItems, onCloseMoveDialog}: UseActionsArgs) => { const collectionsAccessEnabled = isEnabledFeature(Feature.CollectionsAccessEnabled); @@ -115,25 +141,34 @@ export const useActions = ({fetchStructureItems, onCloseMoveDialog}: UseActionsA } if (collectionsAccessEnabled && item.permissions.listAccessBindings) { + const isNewAccessDialogEnabled = isEnabledFeature(Feature.EnableNewAccessDialog); actions.push({ text: , action: () => { - dispatch( - openDialog({ - id: DIALOG_IAM_ACCESS, - props: { - open: true, - resourceId: item.collectionId, - resourceType: ResourceType.Collection, - resourceTitle: item.title, - parentId: item.parentId, - canUpdate: item.permissions.updateAccessBindings, - onClose: () => { - dispatch(closeDialog()); + if (isNewAccessDialogEnabled) { + openAccessDialog(dispatch, { + collectionId: item?.collectionId ?? undefined, + resourceTitle: item?.title, + canUpdateAccessBindings: item?.permissions.updateAccessBindings, + }); + } else { + dispatch( + openDialog({ + id: DIALOG_IAM_ACCESS, + props: { + open: true, + resourceId: item.collectionId, + resourceType: ResourceType.Collection, + resourceTitle: item.title, + parentId: item.parentId, + canUpdate: item.permissions.updateAccessBindings, + onClose: () => { + dispatch(closeDialog()); + }, }, - }, - }), - ); + }), + ); + } }, }); } @@ -293,25 +328,35 @@ export const useActions = ({fetchStructureItems, onCloseMoveDialog}: UseActionsA } if (collectionsAccessEnabled && item.permissions.listAccessBindings) { + const isNewAccessDialogEnabled = isEnabledFeature(Feature.EnableNewAccessDialog); actions.push({ text: , action: () => { - dispatch( - openDialog({ - id: DIALOG_IAM_ACCESS, - props: { - open: true, - resourceId: item.workbookId, - resourceType: ResourceType.Workbook, - resourceTitle: item.title, - parentId: item.collectionId, - canUpdate: item.permissions.updateAccessBindings, - onClose: () => { - dispatch(closeDialog()); + if (isNewAccessDialogEnabled) { + openAccessDialog(dispatch, { + workbookId: item?.workbookId ?? undefined, + collectionId: item?.collectionId ?? undefined, + resourceTitle: item?.title, + canUpdateAccessBindings: item?.permissions.updateAccessBindings, + }); + } else { + dispatch( + openDialog({ + id: DIALOG_IAM_ACCESS, + props: { + open: true, + resourceId: item.workbookId, + resourceType: ResourceType.Workbook, + resourceTitle: item.title, + parentId: item.collectionId, + canUpdate: item.permissions.updateAccessBindings, + onClose: () => { + dispatch(closeDialog()); + }, }, - }, - }), - ); + }), + ); + } }, }); } diff --git a/src/ui/units/collections/components/CollectionPage/hooks/useLayout.tsx b/src/ui/units/collections/components/CollectionPage/hooks/useLayout.tsx index dde68c1ed8..e88e1abfcd 100644 --- a/src/ui/units/collections/components/CollectionPage/hooks/useLayout.tsx +++ b/src/ui/units/collections/components/CollectionPage/hooks/useLayout.tsx @@ -7,6 +7,7 @@ import block from 'bem-cn-lite'; import {I18n} from 'i18n'; import {batch, useDispatch, useSelector} from 'react-redux'; import {useHistory} from 'react-router-dom'; +import {DIALOG_ACCESS} from 'ui/components/AccessDialog'; import {getParentCollectionPath} from 'ui/units/collections-navigation/utils'; import {isEnabledFeature} from 'ui/utils/isEnabledFeature'; @@ -259,24 +260,47 @@ export const useLayout = ({ }} onEditAccessClick={() => { if (collectionsAccessEnabled && curCollectionId && collection) { - dispatch( - openDialog({ - id: DIALOG_IAM_ACCESS, - props: { - open: true, - resourceId: collection.collectionId, - resourceType: ResourceType.Collection, - resourceTitle: collection.title, - parentId: collection.parentId, - canUpdate: Boolean( - collection.permissions?.updateAccessBindings, - ), - onClose: () => { - dispatch(closeDialog()); - }, - }, - }), + const isNewAccessDialogEnabled = isEnabledFeature( + Feature.EnableNewAccessDialog, ); + if (isNewAccessDialogEnabled) { + dispatch( + openDialog({ + id: DIALOG_ACCESS, + props: { + collectionId: collection.collectionId, + resourceTitle: collection.title, + canUpdateAccessBindings: Boolean( + collection.permissions + ?.updateAccessBindings, + ), + onClose: () => { + dispatch(closeDialog()); + }, + }, + }), + ); + } else { + dispatch( + openDialog({ + id: DIALOG_IAM_ACCESS, + props: { + open: true, + resourceId: collection.collectionId, + resourceType: ResourceType.Collection, + resourceTitle: collection.title, + parentId: collection.parentId, + canUpdate: Boolean( + collection.permissions + ?.updateAccessBindings, + ), + onClose: () => { + dispatch(closeDialog()); + }, + }, + }), + ); + } } }} /> diff --git a/src/ui/units/workbooks/components/WorkbookActions/WorkbookActions.tsx b/src/ui/units/workbooks/components/WorkbookActions/WorkbookActions.tsx index 1a985fe88d..22d7a50335 100644 --- a/src/ui/units/workbooks/components/WorkbookActions/WorkbookActions.tsx +++ b/src/ui/units/workbooks/components/WorkbookActions/WorkbookActions.tsx @@ -13,6 +13,7 @@ import {I18N} from 'i18n'; import {useDispatch} from 'react-redux'; import {useHistory, useLocation} from 'react-router-dom'; import {WorkbookPageActionsMoreQA} from 'shared/constants/qa'; +import {DIALOG_ACCESS} from 'ui/components/AccessDialog'; import {DIALOG_EXPORT_WORKBOOK} from 'ui/components/CollectionsStructure/ExportWorkbookDialog/ExportWorkbookDialog'; import {DropdownAction} from 'ui/components/DropdownAction/DropdownAction'; import {closeDialog, openDialog} from 'ui/store/actions/dialog'; @@ -193,6 +194,23 @@ export const WorkbookActions: React.FC = ({workbook, refreshWorkbookInfo} dropdownActions.push([...otherActions]); } + const onOpenAccessDialog = () => { + dispatch( + openDialog({ + id: DIALOG_ACCESS, + props: { + workbookId: workbook?.workbookId ?? undefined, + collectionId: workbook?.collectionId ?? undefined, + resourceTitle: workbook.title, + canUpdateAccessBindings: workbook.permissions.updateAccessBindings, + onClose: () => { + dispatch(closeDialog()); + }, + }, + }), + ); + }; + return (
{Boolean(dropdownActions.length) && ( @@ -210,7 +228,11 @@ export const WorkbookActions: React.FC = ({workbook, refreshWorkbookInfo}