diff --git a/webapp/packages/core-blocks/src/FormControls/Filter.tsx b/webapp/packages/core-blocks/src/FormControls/Filter.tsx index ba7b8166b3..9051db9313 100644 --- a/webapp/packages/core-blocks/src/FormControls/Filter.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Filter.tsx @@ -6,7 +6,6 @@ * you may not use this file except in compliance with the License. */ -import { observable } from 'mobx'; import { observer } from 'mobx-react-lite'; import { useCallback, useEffect, useState } from 'react'; import styled, { css, use } from 'reshadow'; @@ -64,13 +63,7 @@ const innerInputStyle = css` } `; -export interface IFilterState { - filterValue: string; -} - -interface Props { - state?: IFilterState; - onFilter?: (filter: string) => void; +interface BaseProps { toggleMode?: boolean; onToggle?: (status: boolean) => void; placeholder?: string; @@ -78,8 +71,24 @@ interface Props { className?: string; } -export const Filter: React.FC = observer(function Filter({ +type ControlledProps = BaseProps & { + name?: string; + value?: string; + state?: never; + onFilter?: (value: string, name?: string) => void; +}; + +type ObjectsProps = BaseProps & { + name: TKey; + state: TState; + onFilter?: (value: TState[TKey], name: TKey) => void; + value?: never; +}; + +export const Filter: React.FC> = observer(function Filter({ state, + name, + value: valueControlled, onFilter, toggleMode, onToggle, @@ -89,26 +98,34 @@ export const Filter: React.FC = observer(function Filter({ }) { const [inputRef] = useFocus({}); const [toggled, setToggled] = useState(!toggleMode); - const [filterState] = useState(() => state || observable({ filterValue: '' })); - const filter = useCallback((value: string) => { - filterState.filterValue = value; - if (onFilter) { - onFilter(value); + const filter = useCallback((value: string | number, name?: string) => { + value = String(value).trim(); + + if (state && name) { + state[name] = value; } - }, [onFilter, filterState]); + + if (onFilter) { + onFilter(value, name); + } + }, [onFilter, state]); const toggle = useCallback(() => { - setToggled(prev => { - if (prev) { - filter(''); - } - if (onToggle) { - onToggle(!prev); - } - return !prev; - }); - }, [onToggle, filter]); + if (!toggleMode) { + return; + } + + if (toggled) { + filter(''); + } + + setToggled(!toggled); + + if (onToggle) { + onToggle(!toggled); + } + }, [toggleMode, toggled, onToggle, filter]); const onKeyDown = useCallback((event: React.KeyboardEvent) => { if (event.key === 'Enter' || event.keyCode === 13) { @@ -131,7 +148,11 @@ export const Filter: React.FC = observer(function Filter({ } }, [toggled, toggleMode, inputRef]); - useEffect(() => () => { filter(''); }, []); + let value: any = valueControlled; + + if (state && name !== undefined && name in state) { + value = state[name]; + } return styled(useStyles(styles, toggleMode && toggleModeButtonStyle))( = observer(function Filter({ style={innerInputStyle} placeholder={placeholder} disabled={disabled} - value={filterState.filterValue} + name={name} + value={value} onKeyDown={onKeyDown} - onChange={value => filter(String(value).trim())} + onChange={filter} {...use({ toggled })} /> diff --git a/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccess.tsx b/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccess.tsx index 33c9f0ca8b..74ef5c6569 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccess.tsx +++ b/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccess.tsx @@ -23,7 +23,7 @@ import { Group, IconOrImage, } from '@cloudbeaver/core-blocks'; -import { useTranslate } from '@cloudbeaver/core-localization'; +import { TLocalizationToken, useTranslate } from '@cloudbeaver/core-localization'; import { useStyles } from '@cloudbeaver/core-theming'; import { isCloudConnection } from '../../Administration/ConnectionsResource'; @@ -57,7 +57,7 @@ const styles = css` `; interface IInfoItem { - text: string; + text: TLocalizationToken; icon: string; } @@ -81,23 +81,30 @@ export const ConnectionAccess: TabContainerPanelComponent const grantedUsers = useMemo(() => computed(() => users.resource.values .filter(user => state.grantedSubjects.includes(user.userId)) - ), [state.grantedSubjects, users.resource.values]); + ), [state.grantedSubjects, users.resource]); const grantedRoles = useMemo(() => computed(() => roles.resource.values .filter(role => state.grantedSubjects.includes(role.roleId)) - ), [state.grantedSubjects, roles.resource.values]); + ), [state.grantedSubjects, roles.resource]); const { selected } = useTab(tabId, load); const loading = users.isLoading() || roles.isLoading() || state.loading; const cloud = formState.info ? isCloudConnection(formState.info) : false; const disabled = loading || !state.loaded || formState.disabled || cloud; - const infoItem: IInfoItem = { - text: translate('connections_connection_access_save_reminder'), - icon: '/icons/info_icon.svg', - }; + let infoItem: IInfoItem | null = null; + + if (unsaved) { + infoItem = { + text: 'connections_connection_access_save_reminder', + icon: '/icons/info_icon.svg', + }; + } if (cloud) { - infoItem.text = translate('connections_connection_access_cloud_placeholder'); + infoItem = { + text: 'connections_connection_access_cloud_placeholder', + icon: '/icons/info_icon.svg', + }; } if (!selected) { @@ -114,10 +121,10 @@ export const ConnectionAccess: TabContainerPanelComponent ) : ( <> - {(unsaved || cloud) && ( + {infoItem && ( - {infoItem.text} + {translate(infoItem.text)} )} = observer(function Co const translate = useTranslate(); const [selectedSubjects] = useState>(() => observable(new Map())); const [filterState] = useState(() => observable({ filterValue: '' })); - const subjectsSelected = useMemo(() => computed( - () => Array.from(selectedSubjects.values()).some(Boolean) + const selectedList = useMemo(() => computed( + () => Array.from(selectedSubjects.entries()).filter(([key, value]) => value).map(([key]) => key) ), [selectedSubjects]); const revoke = useCallback(() => { - const subjectsToRevoke = []; - for (const [subject, value] of selectedSubjects) { - if (value) { - subjectsToRevoke.push(subject); - } - } - - props.onRevoke(subjectsToRevoke); + props.onRevoke(selectedList.get()); selectedSubjects.clear(); }, []); const roles = useMemo(() => computed(() => getFilteredRoles( grantedRoles, filterState.filterValue - )), [filterState.filterValue, grantedRoles]); + )), [filterState, grantedRoles]); const users = useMemo(() => computed(() => getFilteredUsers( grantedUsers, filterState.filterValue - )), [filterState.filterValue, grantedUsers]); + )), [filterState, grantedUsers]); - let tableInfoText: string = translate('connections_connection_access_admin_info'); + let tableInfoText: TLocalizationToken = 'connections_connection_access_admin_info'; if (!roles.get().length && !users.get().length) { if (filterState.filterValue) { - tableInfoText = translate('connections_connection_access_filter_no_result'); + tableInfoText = 'connections_connection_access_filter_no_result'; } else { - tableInfoText = translate('connections_connection_access_empty_table_placeholder'); + tableInfoText = 'connections_connection_access_empty_table_placeholder'; } } return styled(style)( - - + + @@ -113,7 +102,7 @@ export const ConnectionAccessGrantedList: React.FC = observer(function Co - {tableInfoText} + {translate(tableInfoText)} {roles.get().map(role => ( diff --git a/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccessList.tsx b/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccessList.tsx index 14a859ea9b..ab968b738b 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccessList.tsx +++ b/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccessList.tsx @@ -19,14 +19,13 @@ import { BASE_CONTAINERS_STYLES, Group, Button, - IFilterState, useObjectRef } from '@cloudbeaver/core-blocks'; import { useTranslate } from '@cloudbeaver/core-localization'; import type { AdminRoleInfo, AdminUserInfoFragment } from '@cloudbeaver/core-sdk'; import { useStyles, composes } from '@cloudbeaver/core-theming'; -import { ConnectionAccessTableHeader } from './ConnectionAccessTableHeader/ConnectionAccessTableHeader'; +import { ConnectionAccessTableHeader, IFilterState } from './ConnectionAccessTableHeader/ConnectionAccessTableHeader'; import { ConnectionAccessTableInnerHeader } from './ConnectionAccessTableHeader/ConnectionAccessTableInnerHeader'; import { ConnectionAccessTableItem } from './ConnectionAccessTableitem'; import { getFilteredRoles, getFilteredUsers } from './getFilteredSubjects'; @@ -43,9 +42,6 @@ const styles = composes( position: relative; overflow: auto !important; } - Table { - flex: 1; - } ` ); @@ -69,35 +65,27 @@ export const ConnectionAccessList: React.FC = observer(function Connectio const translate = useTranslate(); const [selectedSubjects] = useState>(() => observable(new Map())); const [filterState] = useState(() => observable({ filterValue: '' })); - const subjectsSelected = useMemo(() => computed( - () => Array.from(selectedSubjects.values()).some(Boolean) + const selectedList = useMemo(() => computed( + () => Array.from(selectedSubjects.entries()).filter(([key, value]) => value).map(([key]) => key) ), [selectedSubjects]); const grant = useCallback(() => { - const subjectsToGrant = []; - - for (const [subject, value] of selectedSubjects) { - if (value) { - subjectsToGrant.push(subject); - } - } - - props.onGrant(subjectsToGrant); + props.onGrant(selectedList.get()); selectedSubjects.clear(); }, []); const roles = useMemo(() => computed(() => getFilteredRoles( roleList, filterState.filterValue - )), [filterState.filterValue, roleList]); + )), [filterState, roleList]); const users = useMemo(() => computed(() => getFilteredUsers( userList, filterState.filterValue - )), [filterState.filterValue, userList]); + )), [filterState, userList]); return styled(style)( - - + +
@@ -116,7 +104,7 @@ export const ConnectionAccessList: React.FC = observer(function Connectio name={role.roleName || ''} description={role.description} icon='/icons/role.svg' - iconTooltip={translate('connections_connection_access_role_tooltip')} + iconTooltip='connections_connection_access_role_tooltip' disabled={disabled || grantedSubjects.includes(role.roleId)} /> ))} diff --git a/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccessTableHeader/ConnectionAccessTableHeader.tsx b/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccessTableHeader/ConnectionAccessTableHeader.tsx index 17ed70ed1f..8a962672f4 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccessTableHeader/ConnectionAccessTableHeader.tsx +++ b/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccessTableHeader/ConnectionAccessTableHeader.tsx @@ -9,12 +9,16 @@ import { observer } from 'mobx-react-lite'; import styled, { css } from 'reshadow'; -import { Filter, IFilterState } from '@cloudbeaver/core-blocks'; +import { Filter } from '@cloudbeaver/core-blocks'; import { useTranslate } from '@cloudbeaver/core-localization'; import { composes, useStyles } from '@cloudbeaver/core-theming'; +export interface IFilterState { + filterValue: string; +} + interface Props { - filter: IFilterState; + filterState: IFilterState; disabled: boolean; className?: string; } @@ -29,7 +33,6 @@ const styles = composes( buttons { display: flex; gap: 16px; - grid-gap: 16px; } header { overflow: hidden; @@ -41,17 +44,21 @@ const styles = composes( justify-content: space-between; padding: 16px; gap: 16px; - grid-gap: 16px; border-bottom: 1px solid; } ` ); -export const ConnectionAccessTableHeader: React.FC = observer(function ConnectionAccessTableHeader({ filter, disabled, className, children }) { +export const ConnectionAccessTableHeader: React.FC = observer(function ConnectionAccessTableHeader({ filterState, disabled, className, children }) { const translate = useTranslate(); return styled(useStyles(styles))(
- + {children} diff --git a/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccessTableItem.tsx b/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccessTableItem.tsx index d5b4acf84b..09b6b7a2a3 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccessTableItem.tsx +++ b/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccessTableItem.tsx @@ -10,13 +10,14 @@ import { observer } from 'mobx-react-lite'; import styled, { css } from 'reshadow'; import { StaticImage, TableColumnValue, TableItem, TableItemSelect } from '@cloudbeaver/core-blocks'; +import { TLocalizationToken, useTranslate } from '@cloudbeaver/core-localization'; interface Props { id: any; name: string; icon: string; disabled: boolean; - iconTooltip?: string; + iconTooltip?: TLocalizationToken; description?: string; className?: string; } @@ -31,6 +32,7 @@ const style = css` export const ConnectionAccessTableItem: React.FC = observer(function ConnectionAccessTableItem({ id, name, description, icon, iconTooltip, disabled, className, }) { + const translate = useTranslate(); return styled(style)( = observer(function Conn - + {name} {description} diff --git a/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/useConnectionAccessState.ts b/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/useConnectionAccessState.ts index 349b1be3ea..3c26bc010b 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/useConnectionAccessState.ts +++ b/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/useConnectionAccessState.ts @@ -32,13 +32,7 @@ export function useConnectionAccessState(connection: DatabaseConnectionFragment }; const revoke = (subjectIds: string[]): void => { - const newGrantedSubjects = []; - for (const subjectId of state.grantedSubjects) { - if (!subjectIds.includes(subjectId)) { - newGrantedSubjects.push(subjectId); - } - } - state.grantedSubjects = newGrantedSubjects; + state.grantedSubjects = state.grantedSubjects.filter(subject => !subjectIds.includes(subject)); }; const grant = (subjectIds: string[]): void => { diff --git a/webapp/packages/core-connections/src/locales/en.ts b/webapp/packages/core-connections/src/locales/en.ts index c3f7725919..0055a1e907 100644 --- a/webapp/packages/core-connections/src/locales/en.ts +++ b/webapp/packages/core-connections/src/locales/en.ts @@ -32,13 +32,13 @@ export default [ ['connections_connection_address', 'Address'], ['connections_connection_name', 'Connection name'], ['connections_connection_access_user_or_role_name', 'User or Role name'], - ['connections_connection_access_save_reminder', 'You have unsaved changes. Click save to save all permission changes.'], + ['connections_connection_access_save_reminder', 'You have unsaved changes. Click Save to save all permission changes.'], ['connections_connection_access_user_tooltip', 'User'], ['connections_connection_access_role_tooltip', 'Role'], ['connections_connection_access_revoke', 'Revoke'], ['connections_connection_access_grant', 'Grant'], ['connections_connection_access_edit', 'Edit'], - ['connections_connection_access_cloud_placeholder', 'Cloud connections are visible to all users. Access management can be configured in AWS console.'], + ['connections_connection_access_cloud_placeholder', 'Cloud connections are visible to all users. Access management can be configured in "AWS Management Console".'], ['connections_connection_access_empty_table_placeholder', 'There are no items yet. Press edit button to grant role or user.'], ['connections_connection_access_filter_placeholder', 'Search for user or role name'], ['connections_connection_access_filter_no_result', 'No results have been found.'], diff --git a/webapp/packages/core-connections/src/locales/ru.ts b/webapp/packages/core-connections/src/locales/ru.ts index 42b57a1a46..86b79822c6 100644 --- a/webapp/packages/core-connections/src/locales/ru.ts +++ b/webapp/packages/core-connections/src/locales/ru.ts @@ -29,14 +29,14 @@ export default [ ['connections_connection_address', 'Адрес'], ['connections_connection_name', 'Название подключения'], ['connections_connection_access_user_or_role_name', 'Имя пользователя или роли'], - ['connections_connection_access_save_reminder', 'У вас есть несохраненные изменения. Нажмите кнопку сохранить, чтобы применить изменения в настройках доступа.'], + ['connections_connection_access_save_reminder', 'У вас есть несохраненные изменения. Нажмите кнопку Сохранить, чтобы применить изменения в настройках доступа.'], ['connections_connection_access_user_tooltip', 'Пользователь'], ['connections_connection_access_role_tooltip', 'Роль'], - ['connections_connection_access_revoke', 'Отозвать'], - ['connections_connection_access_grant', 'Дать доступ'], + ['connections_connection_access_revoke', 'Запретить'], + ['connections_connection_access_grant', 'Разрешить'], ['connections_connection_access_edit', 'Изменить'], - ['connections_connection_access_cloud_placeholder', 'Облачные подключения видны всем пользователям. Вы можете настроить доступ в AWS консоли.'], - ['connections_connection_access_empty_table_placeholder', 'Вы еще ничего не добавили. Нажмите кнопку редактировать, чтобы добавить роль или пользователя.'], + ['connections_connection_access_cloud_placeholder', 'Облачные подключения видны всем пользователям. Вы можете настроить доступ в "Консоль Управления AWS".'], + ['connections_connection_access_empty_table_placeholder', 'Вы еще ничего не добавили. Нажмите кнопку Изменить, чтобы добавить роль или пользователя.'], ['connections_connection_access_filter_placeholder', 'Поиск по имени пользователя или роли'], ['connections_connection_access_filter_no_result', 'По вашему запросу ничего не найдено.'], ['connections_connection_access_admin_info', 'Администраторы видят все подключения.'],