Skip to content

Commit b8512bb

Browse files
committed
feat: grant permissions
1 parent 9898e5f commit b8512bb

36 files changed

+2181
-83
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
.ydb-subject-with-avatar {
2+
&__avatar-wrapper {
3+
position: relative;
4+
}
5+
6+
&__subject {
7+
overflow: hidden;
8+
9+
text-overflow: ellipsis;
10+
}
11+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import {Avatar, Flex, Text} from '@gravity-ui/uikit';
2+
3+
import {cn} from '../../utils/cn';
4+
5+
export const block = cn('ydb-subject-with-avatar');
6+
7+
import './SubjectWithAvatar.scss';
8+
9+
interface SubjectProps {
10+
subject: string;
11+
title?: string;
12+
renderIcon?: () => React.ReactNode;
13+
}
14+
15+
export function SubjectWithAvatar({subject, title, renderIcon}: SubjectProps) {
16+
return (
17+
<Flex gap={2} alignItems="center">
18+
<div className={block('avatar-wrapper')}>
19+
<Avatar theme="brand" text={subject} title={subject} />
20+
{renderIcon?.()}
21+
</div>
22+
<Flex direction="column" overflow="hidden">
23+
<Text variant="body-2" className={block('subject')}>
24+
{subject}
25+
</Text>
26+
{title && (
27+
<Text variant="body-2" color="secondary">
28+
{title}
29+
</Text>
30+
)}
31+
</Flex>
32+
</Flex>
33+
);
34+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
.ydb-access-rights {
2+
$block: &;
3+
&__header {
4+
position: sticky;
5+
left: 0;
6+
7+
margin-bottom: var(--g-spacing-3);
8+
}
9+
&__owner-card {
10+
width: max-content;
11+
padding: var(--g-spacing-2) var(--g-spacing-3);
12+
}
13+
14+
&__icon-wrapper {
15+
position: absolute;
16+
right: -2px;
17+
bottom: -2px;
18+
19+
height: 16px;
20+
21+
color: var(--g-color-base-warning-heavy);
22+
border-radius: 50%;
23+
background: var(--g-color-base-background);
24+
aspect-ratio: 1;
25+
}
26+
&__owner-divider {
27+
height: 24px;
28+
}
29+
&__owner-description {
30+
max-width: 391px;
31+
}
32+
&__dialog-content-wrapper {
33+
position: relative;
34+
35+
height: 46px;
36+
}
37+
&__dialog-error {
38+
position: absolute;
39+
bottom: 0;
40+
left: 0;
41+
42+
overflow: hidden;
43+
44+
max-width: 100%;
45+
46+
white-space: nowrap;
47+
text-overflow: ellipsis;
48+
}
49+
50+
&__note {
51+
display: flex;
52+
.g-help-mark__button {
53+
display: flex;
54+
}
55+
}
56+
&__rights-wrapper {
57+
position: relative;
58+
59+
width: 100%;
60+
height: 100%;
61+
}
62+
&__rights-actions {
63+
position: absolute;
64+
right: 0;
65+
66+
visibility: hidden;
67+
68+
height: 100%;
69+
padding-left: var(--g-spacing-2);
70+
71+
background-color: var(--ydb-data-table-color-hover);
72+
}
73+
&__rights-table {
74+
.data-table__row:hover {
75+
#{$block}__rights-actions {
76+
visibility: visible;
77+
}
78+
}
79+
}
80+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import React from 'react';
2+
3+
import {PersonPlus} from '@gravity-ui/icons';
4+
import {Button, Flex, Icon} from '@gravity-ui/uikit';
5+
6+
import {ResponseError} from '../../../../components/Errors/ResponseError';
7+
import {LoaderWrapper} from '../../../../components/LoaderWrapper/LoaderWrapper';
8+
import {useEditAccessAvailable} from '../../../../store/reducers/capabilities/hooks';
9+
import {schemaAclApi} from '../../../../store/reducers/schemaAcl/schemaAcl';
10+
import {useAutoRefreshInterval} from '../../../../utils/hooks';
11+
import {useCurrentSchema} from '../../TenantContext';
12+
import {useTenantQueryParams} from '../../useTenantQueryParams';
13+
14+
import {Owner} from './components/Owner';
15+
import {RightsTable} from './components/RightsTable/RightsTable';
16+
import i18n from './i18n';
17+
import {block} from './shared';
18+
19+
import './AccessRights.scss';
20+
21+
export function AccessRights() {
22+
const {path, database} = useCurrentSchema();
23+
const editable = useEditAccessAvailable();
24+
const [autoRefreshInterval] = useAutoRefreshInterval();
25+
const {currentData, isFetching, error} = schemaAclApi.useGetSchemaAclQuery(
26+
{path, database},
27+
{
28+
pollingInterval: autoRefreshInterval,
29+
},
30+
);
31+
32+
const {handleShowGrantAccessChange} = useTenantQueryParams();
33+
34+
const loading = isFetching && !currentData;
35+
36+
const renderContent = () => {
37+
if (error) {
38+
return <ResponseError error={error} />;
39+
}
40+
41+
return (
42+
<React.Fragment>
43+
<Flex
44+
gap={10}
45+
justifyContent="space-between"
46+
alignItems="start"
47+
className={block('header')}
48+
>
49+
<Owner />
50+
{editable && (
51+
<Button
52+
view="action"
53+
onClick={() => {
54+
handleShowGrantAccessChange(true);
55+
}}
56+
>
57+
<Icon data={PersonPlus} />
58+
{i18n('action_grant-access')}
59+
</Button>
60+
)}
61+
</Flex>
62+
<RightsTable />
63+
</React.Fragment>
64+
);
65+
};
66+
67+
return <LoaderWrapper loading={loading}>{renderContent()}</LoaderWrapper>;
68+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import React from 'react';
2+
3+
import NiceModal from '@ebay/nice-modal-react';
4+
import {Dialog, Text, TextInput} from '@gravity-ui/uikit';
5+
6+
import {schemaAclApi} from '../../../../../store/reducers/schemaAcl/schemaAcl';
7+
import createToast from '../../../../../utils/createToast';
8+
import {prepareErrorMessage} from '../../../../../utils/prepareErrorMessage';
9+
import i18n from '../i18n';
10+
import {block} from '../shared';
11+
12+
const CHANGE_OWNER_DIALOG = 'change-owner-dialog';
13+
14+
interface GetChangeOwnerDialogProps {
15+
path: string;
16+
database: string;
17+
}
18+
19+
export async function getChangeOwnerDialog({
20+
path,
21+
database,
22+
}: GetChangeOwnerDialogProps): Promise<boolean> {
23+
return await NiceModal.show(CHANGE_OWNER_DIALOG, {
24+
id: CHANGE_OWNER_DIALOG,
25+
path,
26+
database,
27+
});
28+
}
29+
30+
const ChangeOwnerDialogNiceModal = NiceModal.create(
31+
({path, database}: GetChangeOwnerDialogProps) => {
32+
const modal = NiceModal.useModal();
33+
34+
const handleClose = () => {
35+
modal.hide();
36+
modal.remove();
37+
};
38+
39+
return (
40+
<ChangeOwnerDialog
41+
onClose={() => {
42+
modal.resolve(false);
43+
handleClose();
44+
}}
45+
open={modal.visible}
46+
path={path}
47+
database={database}
48+
/>
49+
);
50+
},
51+
);
52+
53+
NiceModal.register(CHANGE_OWNER_DIALOG, ChangeOwnerDialogNiceModal);
54+
55+
interface ChangeOwnerDialogProps extends GetChangeOwnerDialogProps {
56+
open: boolean;
57+
onClose: () => void;
58+
}
59+
60+
function ChangeOwnerDialog({open, onClose, path, database}: ChangeOwnerDialogProps) {
61+
const [newOwner, setNewOwner] = React.useState('');
62+
const [requestErrorMessage, setRequestErrorMessage] = React.useState('');
63+
const [updateOwner, updateOwnerResponse] = schemaAclApi.useUpdateAccessMutation();
64+
65+
const handleTyping = (value: string) => {
66+
setNewOwner(value);
67+
setRequestErrorMessage('');
68+
};
69+
return (
70+
<Dialog open={open} size="s" onClose={onClose}>
71+
<Dialog.Header caption={i18n('action_change-owner')} />
72+
<form
73+
onSubmit={(e) => {
74+
e.preventDefault();
75+
updateOwner({path, database, rights: {ChangeOwnership: {Subject: newOwner}}})
76+
.unwrap()
77+
.then(() => {
78+
onClose();
79+
createToast({
80+
name: 'updateOwner',
81+
content: i18n('title_owner-changed'),
82+
autoHiding: 3000,
83+
});
84+
})
85+
.catch((error) => {
86+
setRequestErrorMessage(prepareErrorMessage(error));
87+
});
88+
}}
89+
>
90+
<Dialog.Body>
91+
<div className={block('dialog-content-wrapper')}>
92+
<TextInput
93+
id="queryName"
94+
placeholder={i18n('decription_enter-subject')}
95+
value={newOwner}
96+
onUpdate={handleTyping}
97+
hasClear
98+
autoFocus
99+
autoComplete={false}
100+
/>
101+
{requestErrorMessage && (
102+
<Text
103+
color="danger"
104+
className={block('dialog-error')}
105+
title={requestErrorMessage}
106+
>
107+
{requestErrorMessage}
108+
</Text>
109+
)}
110+
</div>
111+
</Dialog.Body>
112+
<Dialog.Footer
113+
textButtonCancel={i18n('action_cancel')}
114+
textButtonApply={i18n('action_apply')}
115+
onClickButtonCancel={onClose}
116+
propsButtonApply={{
117+
type: 'submit',
118+
loading: updateOwnerResponse.isLoading,
119+
disabled: !newOwner.length,
120+
}}
121+
/>
122+
</form>
123+
</Dialog>
124+
);
125+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import {CrownDiamond, Pencil} from '@gravity-ui/icons';
2+
import {ActionTooltip, Button, Card, Divider, Flex, Icon, Text} from '@gravity-ui/uikit';
3+
4+
import {SubjectWithAvatar} from '../../../../../components/SubjectWithAvatar/SubjectWithAvatar';
5+
import {useEditAccessAvailable} from '../../../../../store/reducers/capabilities/hooks';
6+
import {selectSchemaOwner} from '../../../../../store/reducers/schemaAcl/schemaAcl';
7+
import {useTypedSelector} from '../../../../../utils/hooks';
8+
import {useCurrentSchema} from '../../../TenantContext';
9+
import i18n from '../i18n';
10+
import {block} from '../shared';
11+
12+
import {getChangeOwnerDialog} from './ChangeOwnerDialog';
13+
14+
export function Owner() {
15+
const editable = useEditAccessAvailable();
16+
const {path, database} = useCurrentSchema();
17+
const owner = useTypedSelector((state) => selectSchemaOwner(state, path, database));
18+
19+
if (!owner) {
20+
return null;
21+
}
22+
23+
const renderIcon = () => {
24+
return (
25+
<Flex alignItems="center" justifyContent="center" className={block('icon-wrapper')}>
26+
<Icon data={CrownDiamond} size={14} className={block('avatar-icon')} />
27+
</Flex>
28+
);
29+
};
30+
return (
31+
<Flex gap={4} alignItems="center">
32+
<Card view="filled" className={block('owner-card')}>
33+
<Flex alignItems="center" justifyContent="space-between" gap={9}>
34+
<SubjectWithAvatar
35+
subject={owner}
36+
renderIcon={renderIcon}
37+
title={i18n('title_owner')}
38+
/>
39+
{editable && (
40+
<Flex gap={1} alignItems="center">
41+
<Divider className={block('owner-divider')} orientation="vertical" />
42+
<ActionTooltip title={i18n('action_change-owner')}>
43+
<Button
44+
view="flat-secondary"
45+
onClick={() => getChangeOwnerDialog({path, database})}
46+
>
47+
<Icon data={Pencil} />
48+
</Button>
49+
</ActionTooltip>
50+
</Flex>
51+
)}
52+
</Flex>
53+
</Card>
54+
<Text color="secondary" className={block('owner-description')}>
55+
{i18n('description_owner')}
56+
</Text>
57+
</Flex>
58+
);
59+
}

0 commit comments

Comments
 (0)