diff --git a/src/containers/Tenant/Diagnostics/AccessRights/AccessRights.tsx b/src/containers/Tenant/Diagnostics/AccessRights/AccessRights.tsx index f785a248dd..98d101f1de 100644 --- a/src/containers/Tenant/Diagnostics/AccessRights/AccessRights.tsx +++ b/src/containers/Tenant/Diagnostics/AccessRights/AccessRights.tsx @@ -7,7 +7,7 @@ import {ResponseError} from '../../../../components/Errors/ResponseError'; import {LoaderWrapper} from '../../../../components/LoaderWrapper/LoaderWrapper'; import {useEditAccessAvailable} from '../../../../store/reducers/capabilities/hooks'; import {schemaAclApi} from '../../../../store/reducers/schemaAcl/schemaAcl'; -import {useAutoRefreshInterval} from '../../../../utils/hooks'; +import {useAclSyntax, useAutoRefreshInterval} from '../../../../utils/hooks'; import {useCurrentSchema} from '../../TenantContext'; import {useTenantQueryParams} from '../../useTenantQueryParams'; @@ -22,8 +22,9 @@ export function AccessRights() { const {path, database} = useCurrentSchema(); const editable = useEditAccessAvailable(); const [autoRefreshInterval] = useAutoRefreshInterval(); - const {isLoading, error} = schemaAclApi.useGetSchemaAclQuery( - {path, database}, + const dialect = useAclSyntax(); + const {isFetching, currentData, error} = schemaAclApi.useGetSchemaAclQuery( + {path, database, dialect}, { pollingInterval: autoRefreshInterval, }, @@ -31,6 +32,8 @@ export function AccessRights() { const {handleShowGrantAccessChange} = useTenantQueryParams(); + const loading = isFetching && !currentData; + const renderContent = () => { if (error) { return ; @@ -62,5 +65,5 @@ export function AccessRights() { ); }; - return {renderContent()}; + return {renderContent()}; } diff --git a/src/containers/Tenant/Diagnostics/AccessRights/components/ChangeOwnerDialog.tsx b/src/containers/Tenant/Diagnostics/AccessRights/components/ChangeOwnerDialog.tsx index 95f865bfe4..9b6e6bb118 100644 --- a/src/containers/Tenant/Diagnostics/AccessRights/components/ChangeOwnerDialog.tsx +++ b/src/containers/Tenant/Diagnostics/AccessRights/components/ChangeOwnerDialog.tsx @@ -5,6 +5,7 @@ import {Dialog, Text, TextInput} from '@gravity-ui/uikit'; import {schemaAclApi} from '../../../../../store/reducers/schemaAcl/schemaAcl'; import createToast from '../../../../../utils/createToast'; +import {useAclSyntax} from '../../../../../utils/hooks'; import {prepareErrorMessage} from '../../../../../utils/prepareErrorMessage'; import i18n from '../i18n'; import {block} from '../shared'; @@ -61,13 +62,14 @@ function ChangeOwnerDialog({open, onClose, path, database}: ChangeOwnerDialogPro const [newOwner, setNewOwner] = React.useState(''); const [requestErrorMessage, setRequestErrorMessage] = React.useState(''); const [updateOwner, updateOwnerResponse] = schemaAclApi.useUpdateAccessMutation(); + const dialect = useAclSyntax(); const handleTyping = (value: string) => { setNewOwner(value); setRequestErrorMessage(''); }; const onApply = () => { - updateOwner({path, database, rights: {ChangeOwnership: {Subject: newOwner}}}) + updateOwner({path, database, dialect, rights: {ChangeOwnership: {Subject: newOwner}}}) .unwrap() .then(() => { onClose(); diff --git a/src/containers/Tenant/Diagnostics/AccessRights/components/Owner.tsx b/src/containers/Tenant/Diagnostics/AccessRights/components/Owner.tsx index 3c25bb31d4..a535e2b057 100644 --- a/src/containers/Tenant/Diagnostics/AccessRights/components/Owner.tsx +++ b/src/containers/Tenant/Diagnostics/AccessRights/components/Owner.tsx @@ -4,7 +4,7 @@ import {ActionTooltip, Button, Card, Divider, Flex, Icon, Text} from '@gravity-u import {SubjectWithAvatar} from '../../../../../components/SubjectWithAvatar/SubjectWithAvatar'; import {useEditAccessAvailable} from '../../../../../store/reducers/capabilities/hooks'; import {selectSchemaOwner} from '../../../../../store/reducers/schemaAcl/schemaAcl'; -import {useTypedSelector} from '../../../../../utils/hooks'; +import {useAclSyntax, useTypedSelector} from '../../../../../utils/hooks'; import {useCurrentSchema} from '../../../TenantContext'; import i18n from '../i18n'; import {block} from '../shared'; @@ -14,7 +14,8 @@ import {getChangeOwnerDialog} from './ChangeOwnerDialog'; export function Owner() { const editable = useEditAccessAvailable(); const {path, database} = useCurrentSchema(); - const owner = useTypedSelector((state) => selectSchemaOwner(state, path, database)); + const dialect = useAclSyntax(); + const owner = useTypedSelector((state) => selectSchemaOwner(state, path, database, dialect)); if (!owner) { return null; diff --git a/src/containers/Tenant/Diagnostics/AccessRights/components/RightsTable/Actions.tsx b/src/containers/Tenant/Diagnostics/AccessRights/components/RightsTable/Actions.tsx index fc9cfb1b57..40d9a28e92 100644 --- a/src/containers/Tenant/Diagnostics/AccessRights/components/RightsTable/Actions.tsx +++ b/src/containers/Tenant/Diagnostics/AccessRights/components/RightsTable/Actions.tsx @@ -3,7 +3,7 @@ import {ActionTooltip, Button, Flex, Icon} from '@gravity-ui/uikit'; import {useEditAccessAvailable} from '../../../../../../store/reducers/capabilities/hooks'; import {selectSubjectExplicitRights} from '../../../../../../store/reducers/schemaAcl/schemaAcl'; -import {useTypedSelector} from '../../../../../../utils/hooks'; +import {useAclSyntax, useTypedSelector} from '../../../../../../utils/hooks'; import {useCurrentSchema} from '../../../../TenantContext'; import {useTenantQueryParams} from '../../../../useTenantQueryParams'; import i18n from '../../i18n'; @@ -50,8 +50,9 @@ function GrantRightsToSubject({subject}: ActionProps) { function RevokeAllRights({subject}: ActionProps) { const {path, database} = useCurrentSchema(); + const dialect = useAclSyntax(); const subjectExplicitRights = useTypedSelector((state) => - selectSubjectExplicitRights(state, subject, path, database), + selectSubjectExplicitRights(state, subject, path, database, dialect), ); const noRightsToRevoke = subjectExplicitRights.length === 0; diff --git a/src/containers/Tenant/Diagnostics/AccessRights/components/RightsTable/RevokeAllRightsDialog.tsx b/src/containers/Tenant/Diagnostics/AccessRights/components/RightsTable/RevokeAllRightsDialog.tsx index 78b9744193..90635282cb 100644 --- a/src/containers/Tenant/Diagnostics/AccessRights/components/RightsTable/RevokeAllRightsDialog.tsx +++ b/src/containers/Tenant/Diagnostics/AccessRights/components/RightsTable/RevokeAllRightsDialog.tsx @@ -9,7 +9,7 @@ import { selectSubjectExplicitRights, } from '../../../../../../store/reducers/schemaAcl/schemaAcl'; import createToast from '../../../../../../utils/createToast'; -import {useTypedSelector} from '../../../../../../utils/hooks'; +import {useAclSyntax, useTypedSelector} from '../../../../../../utils/hooks'; import {prepareErrorMessage} from '../../../../../../utils/prepareErrorMessage'; import i18n from '../../i18n'; @@ -72,8 +72,9 @@ function RevokeAllRightsDialog({ database, subject, }: RevokeAllRightsDialogProps) { + const dialect = useAclSyntax(); const subjectExplicitRights = useTypedSelector((state) => - selectSubjectExplicitRights(state, subject, path, database), + selectSubjectExplicitRights(state, subject, path, database, dialect), ); const [requestErrorMessage, setRequestErrorMessage] = React.useState(''); @@ -83,6 +84,7 @@ function RevokeAllRightsDialog({ removeAccess({ path, database, + dialect, rights: { RemoveAccess: [ { diff --git a/src/containers/Tenant/Diagnostics/AccessRights/components/RightsTable/RightsTable.tsx b/src/containers/Tenant/Diagnostics/AccessRights/components/RightsTable/RightsTable.tsx index 71a7c89828..30bbc3c634 100644 --- a/src/containers/Tenant/Diagnostics/AccessRights/components/RightsTable/RightsTable.tsx +++ b/src/containers/Tenant/Diagnostics/AccessRights/components/RightsTable/RightsTable.tsx @@ -3,7 +3,7 @@ import type {Settings} from '@gravity-ui/react-data-table'; import {ResizeableDataTable} from '../../../../../../components/ResizeableDataTable/ResizeableDataTable'; import {selectPreparedRights} from '../../../../../../store/reducers/schemaAcl/schemaAcl'; import {DEFAULT_TABLE_SETTINGS} from '../../../../../../utils/constants'; -import {useTypedSelector} from '../../../../../../utils/hooks'; +import {useAclSyntax, useTypedSelector} from '../../../../../../utils/hooks'; import {useCurrentSchema} from '../../../../TenantContext'; import i18n from '../../i18n'; import {block} from '../../shared'; @@ -16,7 +16,8 @@ const AccessRightsTableSettings: Settings = {...DEFAULT_TABLE_SETTINGS, dynamicR export function RightsTable() { const {path, database} = useCurrentSchema(); - const data = useTypedSelector((state) => selectPreparedRights(state, path, database)); + const dialect = useAclSyntax(); + const data = useTypedSelector((state) => selectPreparedRights(state, path, database, dialect)); return ( ('Groups'); const {path, database} = useCurrentSchema(); + const dialect = useAclSyntax(); const {currentRightsMap, setExplicitRightsChanges, rightsToGrant, rightsToRevoke, hasChanges} = useRights({aclSubject: aclSubject ?? undefined, path, database}); const {isFetching: aclIsFetching} = schemaAclApi.useGetSchemaAclQuery( { path, database, + dialect, }, {skip: !aclSubject}, ); const {isFetching: availableRightsAreFetching} = schemaAclApi.useGetAvailablePermissionsQuery({ database, + dialect, }); const [updateRights, updateRightsResponse] = schemaAclApi.useUpdateAccessMutation(); const [updateRightsError, setUpdateRightsError] = React.useState(''); const inheritedRightsSet = useTypedSelector((state) => - selectSubjectInheritedRights(state, aclSubject ?? undefined, path, database), + selectSubjectInheritedRights(state, aclSubject ?? undefined, path, database, dialect), ); const handleDiscardRightsChanges = React.useCallback(() => { @@ -68,6 +71,7 @@ export function GrantAccess({handleCloseDrawer}: GrantAccessProps) { updateRights({ path, database, + dialect, rights: { AddAccess: subjects.map((subj) => ({ AccessRights: rightsToGrant, @@ -98,6 +102,7 @@ export function GrantAccess({handleCloseDrawer}: GrantAccessProps) { updateRights, path, database, + dialect, rightsToGrant, aclSubject, rightsToRevoke, @@ -106,7 +111,7 @@ export function GrantAccess({handleCloseDrawer}: GrantAccessProps) { ]); const availablePermissions = useTypedSelector((state) => - selectAvailablePermissions(state, database), + selectAvailablePermissions(state, database, dialect), ); const handleChangeRightGetter = React.useCallback( (right: string) => { diff --git a/src/containers/Tenant/GrantAccess/utils.ts b/src/containers/Tenant/GrantAccess/utils.ts index 9814b0c049..c0a4177dd4 100644 --- a/src/containers/Tenant/GrantAccess/utils.ts +++ b/src/containers/Tenant/GrantAccess/utils.ts @@ -1,7 +1,7 @@ import React from 'react'; import {selectSubjectExplicitRights} from '../../../store/reducers/schemaAcl/schemaAcl'; -import {useTypedSelector} from '../../../utils/hooks'; +import {useAclSyntax, useTypedSelector} from '../../../utils/hooks'; interface UseRightsProps { aclSubject?: string; @@ -10,8 +10,9 @@ interface UseRightsProps { } export function useRights({aclSubject, path, database}: UseRightsProps) { + const dialect = useAclSyntax(); const subjectExplicitRights = useTypedSelector((state) => - selectSubjectExplicitRights(state, aclSubject ?? undefined, path, database), + selectSubjectExplicitRights(state, aclSubject ?? undefined, path, database, dialect), ); const [explicitRightsChanges, setExplicitRightsChanges] = React.useState( () => new Map(), diff --git a/src/containers/UserSettings/i18n/en.json b/src/containers/UserSettings/i18n/en.json index f8788754eb..3ca13000ef 100644 --- a/src/containers/UserSettings/i18n/en.json +++ b/src/containers/UserSettings/i18n/en.json @@ -48,5 +48,11 @@ "settings.useClusterBalancerAsBackend.title": "Use cluster balancer as backend", "settings.useClusterBalancerAsBackend.description": "By default random cluster node is used as backend. It causes saved links to become invalid after some time, when node is restarted. Using balancer as backend fixes it", + "settings.aclSyntax.title": "ACL syntax format", + "settings.aclSyntax.option-kikimr": "KiKiMr", + "settings.aclSyntax.option-ydb-short": "YDB Short", + "settings.aclSyntax.option-ydb": "YDB", + "settings.aclSyntax.option-yql": "YQL", + "settings.about.interfaceVersionInfoField.title": "Interface version" } diff --git a/src/containers/UserSettings/settings.tsx b/src/containers/UserSettings/settings.tsx index 3170cdb7bd..049c0452c7 100644 --- a/src/containers/UserSettings/settings.tsx +++ b/src/containers/UserSettings/settings.tsx @@ -4,7 +4,9 @@ import {createNextState} from '@reduxjs/toolkit'; import {codeAssistBackend} from '../../store'; import { + ACL_SYNTAX_KEY, AUTOCOMPLETE_ON_ENTER, + AclSyntax, BINARY_DATA_IN_PLAIN_TEXT_DISPLAY, ENABLE_AUTOCOMPLETE, ENABLE_CODE_ASSISTANT, @@ -147,6 +149,32 @@ export const autocompleteOnEnterSetting: SettingProps = { description: i18n('settings.editor.autocomplete-on-enter.description'), }; +const aclSyntaxOptions = [ + { + value: AclSyntax.Kikimr, + content: i18n('settings.aclSyntax.option-kikimr'), + }, + { + value: AclSyntax.YdbShort, + content: i18n('settings.aclSyntax.option-ydb-short'), + }, + { + value: AclSyntax.Ydb, + content: i18n('settings.aclSyntax.option-ydb'), + }, + { + value: AclSyntax.Yql, + content: i18n('settings.aclSyntax.option-yql'), + }, +]; + +export const aclSyntaxSetting: SettingProps = { + settingKey: ACL_SYNTAX_KEY, + title: i18n('settings.aclSyntax.title'), + type: 'radio', + options: aclSyntaxOptions, +}; + export const interfaceVersionInfoField: SettingsInfoFieldProps = { title: i18n('settings.about.interfaceVersionInfoField.title'), type: 'info', @@ -161,6 +189,7 @@ export const appearanceSection: SettingsSection = { invertedDisksSetting, binaryDataInPlainTextDisplay, showDomainDatabase, + aclSyntaxSetting, ], }; diff --git a/src/services/api/viewer.ts b/src/services/api/viewer.ts index 5c3f4a6d10..3ff38a1665 100644 --- a/src/services/api/viewer.ts +++ b/src/services/api/viewer.ts @@ -40,8 +40,8 @@ import {BINARY_DATA_IN_PLAIN_TEXT_DISPLAY} from '../../utils/constants'; import type {Nullable} from '../../utils/typecheckers'; import {settingsManager} from '../settings'; -import {BaseYdbAPI} from './base'; import type {AxiosOptions} from './base'; +import {BaseYdbAPI} from './base'; export class ViewerAPI extends BaseYdbAPI { getClusterCapabilities({database}: {database?: string}) { @@ -189,7 +189,7 @@ export class ViewerAPI extends BaseYdbAPI { } getSchemaAcl( - {path, database}: {path: string; database: string}, + {path, database, dialect}: {path: string; database: string; dialect: string}, {concurrentId, signal}: AxiosOptions = {}, ) { return this.get( @@ -198,13 +198,13 @@ export class ViewerAPI extends BaseYdbAPI { database, path, merge_rules: true, - dialect: 'ydb-short', + dialect, }, {concurrentId, requestConfig: {signal}}, ); } getAvailablePermissions( - {path, database}: {path: string; database: string}, + {path, database, dialect}: {path: string; database: string; dialect: string}, {concurrentId, signal}: AxiosOptions = {}, ) { return this.get( @@ -213,7 +213,7 @@ export class ViewerAPI extends BaseYdbAPI { database, path, merge_rules: true, - dialect: 'ydb-short', + dialect, list_permissions: true, }, {concurrentId, requestConfig: {signal}}, @@ -224,7 +224,8 @@ export class ViewerAPI extends BaseYdbAPI { path, database, rights, - }: {path: string; database: string; rights: AccessRightsUpdateRequest}, + dialect, + }: {path: string; database: string; rights: AccessRightsUpdateRequest; dialect: string}, {concurrentId, signal}: AxiosOptions = {}, ) { return this.post( @@ -234,7 +235,7 @@ export class ViewerAPI extends BaseYdbAPI { database, path, merge_rules: true, - dialect: 'ydb-short', + dialect, }, {concurrentId, requestConfig: {signal}}, ); diff --git a/src/services/settings.ts b/src/services/settings.ts index e4f70a9ff0..1c61be61e1 100644 --- a/src/services/settings.ts +++ b/src/services/settings.ts @@ -1,8 +1,10 @@ import {TENANT_PAGES_IDS} from '../store/reducers/tenant/constants'; import { + ACL_SYNTAX_KEY, ASIDE_HEADER_COMPACT_KEY, AUTOCOMPLETE_ON_ENTER, AUTO_REFRESH_INTERVAL, + AclSyntax, BINARY_DATA_IN_PLAIN_TEXT_DISPLAY, CASE_SENSITIVE_JSON_SEARCH, ENABLE_AUTOCOMPLETE, @@ -60,6 +62,7 @@ export const DEFAULT_USER_SETTINGS = { [LAST_QUERY_EXECUTION_SETTINGS_KEY]: undefined, [QUERY_SETTINGS_BANNER_LAST_CLOSED_KEY]: undefined, [QUERY_EXECUTION_SETTINGS_KEY]: DEFAULT_QUERY_SETTINGS, + [ACL_SYNTAX_KEY]: AclSyntax.YdbShort, } as const satisfies SettingsObject; class SettingsManager { diff --git a/src/store/reducers/schemaAcl/schemaAcl.ts b/src/store/reducers/schemaAcl/schemaAcl.ts index 7962f7569e..230d3f7d2b 100644 --- a/src/store/reducers/schemaAcl/schemaAcl.ts +++ b/src/store/reducers/schemaAcl/schemaAcl.ts @@ -7,9 +7,23 @@ import {api} from '../api'; export const schemaAclApi = api.injectEndpoints({ endpoints: (build) => ({ getSchemaAcl: build.query({ - queryFn: async ({path, database}: {path: string; database: string}, {signal}) => { + queryFn: async ( + { + path, + database, + dialect, + }: { + path: string; + database: string; + dialect: string; + }, + {signal}, + ) => { try { - const data = await window.api.viewer.getSchemaAcl({path, database}, {signal}); + const data = await window.api.viewer.getSchemaAcl( + {path, database, dialect}, + {signal}, + ); return { data: { acl: data.Common.ACL, @@ -25,10 +39,19 @@ export const schemaAclApi = api.injectEndpoints({ providesTags: ['All', 'AccessRights'], }), getAvailablePermissions: build.query({ - queryFn: async ({database}: {database: string}, {signal}) => { + queryFn: async ( + { + database, + dialect, + }: { + database: string; + dialect: string; + }, + {signal}, + ) => { try { const data = await window.api.viewer.getAvailablePermissions( - {path: database, database}, + {path: database, database, dialect}, {signal}, ); @@ -45,6 +68,7 @@ export const schemaAclApi = api.injectEndpoints({ database: string; path: string; rights: AccessRightsUpdateRequest; + dialect: string; }) => { try { const data = await window.api.viewer.updateAccessRights(props); @@ -62,82 +86,97 @@ export const schemaAclApi = api.injectEndpoints({ const createGetSchemaAclSelector = createSelector( (path: string) => path, (_path: string, database: string) => database, - (path, database) => schemaAclApi.endpoints.getSchemaAcl.select({path, database}), + (_path: string, _database: string, dialect: string) => dialect, + (path, database, dialect) => + schemaAclApi.endpoints.getSchemaAcl.select({path, database, dialect}), ); export const selectSchemaOwner = createSelector( (state: RootState) => state, - (_state: RootState, path: string, database: string) => - createGetSchemaAclSelector(path, database), + (_state: RootState, path: string, database: string, dialect: string) => + createGetSchemaAclSelector(path, database, dialect), (state, selectGetSchemaAcl) => selectGetSchemaAcl(state).data?.owner, ); const selectAccessRights = createSelector( (state: RootState) => state, - (_state: RootState, path: string, database: string) => - createGetSchemaAclSelector(path, database), + (_state: RootState, path: string, database: string, dialect: string) => + createGetSchemaAclSelector(path, database, dialect), (state, selectGetSchemaAcl) => selectGetSchemaAcl(state).data, ); -const selectRightsMap = createSelector(selectAccessRights, (data) => { - if (!data) { - return null; - } - const {acl, effectiveAcl} = data; - - const result: Record; effective: Set}> = {}; - - if (acl?.length) { - acl.forEach((aclItem) => { - if (aclItem.Subject) { - if (!result[aclItem.Subject]) { - result[aclItem.Subject] = {explicit: new Set(), effective: new Set()}; +const selectRightsMap = createSelector( + (state: RootState, path: string, database: string, dialect: string) => + selectAccessRights(state, path, database, dialect), + (data) => { + if (!data) { + return null; + } + const {acl, effectiveAcl} = data; + + const result: Record; effective: Set}> = {}; + + if (acl?.length) { + acl.forEach((aclItem) => { + if (aclItem.Subject) { + if (!result[aclItem.Subject]) { + result[aclItem.Subject] = {explicit: new Set(), effective: new Set()}; + } + aclItem.AccessRules?.forEach((rule) => { + result[aclItem.Subject].explicit.add(rule); + }); + aclItem.AccessRights?.forEach((rule) => { + result[aclItem.Subject].explicit.add(rule); + }); } - aclItem.AccessRules?.forEach((rule) => { - result[aclItem.Subject].explicit.add(rule); - }); - aclItem.AccessRights?.forEach((rule) => { - result[aclItem.Subject].explicit.add(rule); - }); - } - }); - } - - if (effectiveAcl?.length) { - effectiveAcl.forEach((aclItem) => { - if (aclItem.Subject) { - if (!result[aclItem.Subject]) { - result[aclItem.Subject] = {explicit: new Set(), effective: new Set()}; + }); + } + + if (effectiveAcl?.length) { + effectiveAcl.forEach((aclItem) => { + if (aclItem.Subject) { + if (!result[aclItem.Subject]) { + result[aclItem.Subject] = {explicit: new Set(), effective: new Set()}; + } + aclItem.AccessRules?.forEach((rule) => { + result[aclItem.Subject].effective.add(rule); + }); + aclItem.AccessRights?.forEach((rule) => { + result[aclItem.Subject].effective.add(rule); + }); } - aclItem.AccessRules?.forEach((rule) => { - result[aclItem.Subject].effective.add(rule); - }); - aclItem.AccessRights?.forEach((rule) => { - result[aclItem.Subject].effective.add(rule); - }); - } - }); - } - - return result; -}); + }); + } -export const selectPreparedRights = createSelector(selectRightsMap, (data) => { - if (!data) { - return null; - } - return Object.entries(data).map(([subject, value]) => ({ - subject, - explicit: Array.from(value.explicit), - effective: Array.from(value.effective), - })); -}); + return result; + }, +); + +export const selectPreparedRights = createSelector( + (state: RootState, path: string, database: string, dialect: string) => + selectRightsMap(state, path, database, dialect), + (data) => { + if (!data) { + return null; + } + return Object.entries(data).map(([subject, value]) => ({ + subject, + explicit: Array.from(value.explicit), + effective: Array.from(value.effective), + })); + }, +); export const selectSubjectExplicitRights = createSelector( [ (_state: RootState, subject: string | undefined) => subject, - (state: RootState, _subject: string | undefined, path: string, database: string) => - selectRightsMap(state, path, database), + ( + state: RootState, + _subject: string | undefined, + path: string, + database: string, + dialect: string, + ) => selectRightsMap(state, path, database, dialect), ], (subject, rightsMap) => { if (!subject || !rightsMap) { @@ -152,8 +191,13 @@ export const selectSubjectExplicitRights = createSelector( export const selectSubjectInheritedRights = createSelector( [ (_state: RootState, subject: string | undefined) => subject, - (state: RootState, _subject: string | undefined, path: string, database: string) => - selectRightsMap(state, path, database), + ( + state: RootState, + _subject: string | undefined, + path: string, + database: string, + dialect: string, + ) => selectRightsMap(state, path, database, dialect), ], (subject, rightsMap) => { if (!subject || !rightsMap) { @@ -172,12 +216,15 @@ export const selectSubjectInheritedRights = createSelector( const createGetAvailablePermissionsSelector = createSelector( (database: string) => database, - (database) => schemaAclApi.endpoints.getAvailablePermissions.select({database}), + (_database: string, dialect: string) => dialect, + (database, dialect) => + schemaAclApi.endpoints.getAvailablePermissions.select({database, dialect}), ); // Then create the main selector that extracts the available permissions data export const selectAvailablePermissions = createSelector( (state: RootState) => state, - (_state: RootState, database: string) => createGetAvailablePermissionsSelector(database), + (_state: RootState, database: string, dialect: string) => + createGetAvailablePermissionsSelector(database, dialect), (state, selectGetAvailablePermissions) => selectGetAvailablePermissions(state).data, ); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 70d9ba8212..397b98874b 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -134,3 +134,12 @@ export const DEV_ENABLE_TRACING_FOR_ALL_REQUESTS = 'enable_tracing_for_all_reque export const SHOW_NETWORK_UTILIZATION = 'enableNetworkUtilization'; export const EXPAND_CLUSTER_DASHBOARD = 'expandClusterDashboard'; + +export const ACL_SYNTAX_KEY = 'aclSyntax'; + +export enum AclSyntax { + Kikimr = 'kikimr', + YdbShort = 'ydb-short', + Ydb = 'ydb', + Yql = 'yql', +} diff --git a/src/utils/hooks/index.ts b/src/utils/hooks/index.ts index 59b698f822..0faf3493a9 100644 --- a/src/utils/hooks/index.ts +++ b/src/utils/hooks/index.ts @@ -6,3 +6,5 @@ export * from './useTableSort'; export * from './useSearchQuery'; export * from './useAutoRefreshInterval'; export * from './useEventHandler'; +export * from './useDelayed'; +export * from './useAclSyntax'; diff --git a/src/utils/hooks/useAclSyntax.ts b/src/utils/hooks/useAclSyntax.ts new file mode 100644 index 0000000000..41d91da6bc --- /dev/null +++ b/src/utils/hooks/useAclSyntax.ts @@ -0,0 +1,10 @@ +import {ACL_SYNTAX_KEY, AclSyntax} from '../constants'; + +import {useTypedSelector} from './useTypedSelector'; + +export function useAclSyntax(): string { + const aclSyntax = useTypedSelector( + (state) => state.settings.userSettings[ACL_SYNTAX_KEY] as string | undefined, + ); + return aclSyntax ?? AclSyntax.YdbShort; +} diff --git a/tests/suites/sidebar/Sidebar.ts b/tests/suites/sidebar/Sidebar.ts index dedc3eb8f7..c669b7db3c 100644 --- a/tests/suites/sidebar/Sidebar.ts +++ b/tests/suites/sidebar/Sidebar.ts @@ -69,6 +69,7 @@ export class Sidebar { async clickSettings() { await this.settingsButton.click(); + await this.drawer.waitFor({state: 'visible'}); } async clickInformation() { @@ -176,4 +177,59 @@ export class Sidebar { const switchControl = experimentItem.locator('xpath=../../..//input[@type="checkbox"]'); return switchControl.isChecked(); } + + async closeDrawer(): Promise { + await this.drawer.page().keyboard.press('Escape'); + await this.drawer.waitFor({state: 'hidden'}); + } + + // ACL Syntax methods + async getAclSyntaxRadioGroup(): Promise { + // First, find the settings item that contains the ACL syntax title + const aclSettingsItem = this.drawer + .locator('.gn-settings__item') + .filter({hasText: 'ACL syntax format'}); + + // Then find the radio button group within that item + return aclSettingsItem.locator('.g-radio-button'); + } + + async getSelectedAclSyntax(): Promise { + const radioGroup = await this.getAclSyntaxRadioGroup(); + const checkedOption = radioGroup.locator('.g-radio-button__option_checked'); + const text = await checkedOption.textContent(); + return text?.trim() || ''; + } + + async selectAclSyntax(syntax: 'KiKiMr' | 'YDB Short' | 'YDB' | 'YQL'): Promise { + // Ensure drawer is visible first + await this.drawer.waitFor({state: 'visible'}); + + const radioGroup = await this.getAclSyntaxRadioGroup(); + const option = radioGroup.locator(`.g-radio-button__option:has-text("${syntax}")`); + await option.click(); + // Small delay to ensure the setting is saved + await this.drawer.page().waitForTimeout(100); + } + + async getAclSyntaxOptions(): Promise { + const radioGroup = await this.getAclSyntaxRadioGroup(); + const options = radioGroup.locator('.g-radio-button__option'); + const count = await options.count(); + const texts: string[] = []; + for (let i = 0; i < count; i++) { + const text = await options.nth(i).textContent(); + if (text) { + texts.push(text.trim()); + } + } + return texts; + } + + async isAclSyntaxSettingVisible(): Promise { + const aclSetting = this.drawer + .locator('.gn-settings__item') + .filter({hasText: 'ACL syntax format'}); + return aclSetting.isVisible(); + } } diff --git a/tests/suites/tenant/diagnostics/Diagnostics.ts b/tests/suites/tenant/diagnostics/Diagnostics.ts index d6baaea3a0..e34618ea31 100644 --- a/tests/suites/tenant/diagnostics/Diagnostics.ts +++ b/tests/suites/tenant/diagnostics/Diagnostics.ts @@ -3,6 +3,7 @@ import type {Locator, Page} from '@playwright/test'; import {retryAction} from '../../../utils/retryAction'; import {MemoryViewer} from '../../memoryViewer/MemoryViewer'; import {NodesPage} from '../../nodes/NodesPage'; +import type {Sidebar} from '../../sidebar/Sidebar'; import {StoragePage} from '../../storage/StoragePage'; import {VISIBILITY_TIMEOUT} from '../TenantPage'; @@ -229,6 +230,33 @@ export const TopShardsHistoricalColumns = [ TOP_SHARDS_COLUMNS_IDS.IntervalEnd, ]; +export const ACL_SYNTAX_TEST_CONFIGS = [ + { + syntax: 'YQL' as const, + patterns: { + USERS: 'CONNECT', + 'METADATA-READERS': 'LIST', + 'DATA-READERS': 'SELECT ROW', + }, + }, + { + syntax: 'KiKiMr' as const, + patterns: { + USERS: 'ConnectDatabase', + 'METADATA-READERS': 'List', + 'DATA-READERS': 'SelectRow', + }, + }, + { + syntax: 'YDB Short' as const, + patterns: { + USERS: 'connect', + 'METADATA-READERS': 'list', + 'DATA-READERS': 'select_row', + }, + }, +]; + export class Diagnostics { table: Table; storage: StoragePage; @@ -536,4 +564,71 @@ export class Diagnostics { .waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); return true; } + + async getEffectiveRightsFromTable(): Promise> { + const rightsTable = this.page.locator('.ydb-access-rights__rights-table'); + await rightsTable.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); + + const rows = await rightsTable.locator('tbody tr').all(); + const rights: Record = {}; + + for (const row of rows) { + const cells = await row.locator('td').all(); + if (cells.length >= 3) { + // Get the subject name from the avatar component + const subjectElement = cells[0].locator('.ydb-subject-with-avatar__subject'); + const subject = await subjectElement.textContent(); + const effectiveRights = await cells[2].textContent(); + if (subject && effectiveRights) { + rights[subject.trim()] = effectiveRights.trim(); + } + } + } + + return rights; + } + + async waitForTableDataToLoad(): Promise { + // Wait for the table to be visible and have at least one row of data + const rightsTable = this.page.locator('.ydb-access-rights__rights-table'); + await rightsTable.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); + await rightsTable + .locator('tbody tr') + .first() + .waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); + // Additional small delay to ensure data is fully loaded + await this.page.waitForTimeout(500); + } + + async getPermissionLabelsInGrantDialog(): Promise { + const labels = await this.page + .locator('.ydb-grant-access__rights-wrapper .g-switch__text') + .all(); + const texts: string[] = []; + for (const label of labels) { + const text = await label.textContent(); + if (text) { + texts.push(text.trim()); + } + } + return texts; + } + + async switchAclSyntaxAndGetRights( + sidebar: Sidebar, + syntax: 'KiKiMr' | 'YDB Short' | 'YDB' | 'YQL', + ): Promise> { + // Switch syntax + await sidebar.clickSettings(); + await sidebar.selectAclSyntax(syntax); + await sidebar.closeDrawer(); + + // Refresh the page data by navigating away and back + await this.clickTab(DiagnosticsTab.Info); + await this.clickTab(DiagnosticsTab.Access); + await this.waitForTableDataToLoad(); + + // Get and return the rights + return await this.getEffectiveRightsFromTable(); + } } diff --git a/tests/suites/tenant/diagnostics/tabs/access.test.ts b/tests/suites/tenant/diagnostics/tabs/access.test.ts index eaa37c522a..d64d4f315d 100644 --- a/tests/suites/tenant/diagnostics/tabs/access.test.ts +++ b/tests/suites/tenant/diagnostics/tabs/access.test.ts @@ -1,7 +1,8 @@ import {expect, test} from '@playwright/test'; +import {Sidebar} from '../../../sidebar/Sidebar'; import {TenantPage} from '../../TenantPage'; -import {Diagnostics, DiagnosticsTab} from '../Diagnostics'; +import {ACL_SYNTAX_TEST_CONFIGS, Diagnostics, DiagnosticsTab} from '../Diagnostics'; const newSubject = 'foo'; @@ -127,4 +128,38 @@ test.describe('Diagnostics Access tab', async () => { // Verify that "foo" appears in the rights table await expect(diagnostics.isSubjectInRightsTable(newSubject)).resolves.toBe(true); }); + + test('Effective rights display changes when switching ACL syntax', async ({page}) => { + const pageQueryParams = { + schema: '/local', + database: '/local', + tenantPage: 'diagnostics', + diagnosticsTab: 'access', + }; + const tenantPage = new TenantPage(page); + await tenantPage.goto(pageQueryParams); + + const diagnostics = new Diagnostics(page); + const sidebar = new Sidebar(page); + + // Run tests for each syntax configuration + const results: Record> = {}; + + for (const config of ACL_SYNTAX_TEST_CONFIGS) { + const rights = await diagnostics.switchAclSyntaxAndGetRights(sidebar, config.syntax); + expect(rights).toBeTruthy(); + + // Verify expected patterns + for (const [subject, pattern] of Object.entries(config.patterns)) { + expect(rights[subject]).toContain(pattern); + } + + results[config.syntax] = rights; + } + + // Verify that permission formats are different between syntaxes + expect(results.YQL?.['USERS']).not.toEqual(results.KiKiMr?.['USERS']); + expect(results.KiKiMr?.['USERS']).not.toEqual(results['YDB Short']?.['USERS']); + expect(results.YQL?.['DATA-READERS']).not.toEqual(results['YDB Short']?.['DATA-READERS']); + }); });