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']);
+ });
});