refactor(core-connections): CB-1037 change Filter state managment

This commit is contained in:
Naumov Alexey
2021-06-09 13:31:47 +03:00
parent 7aafe48906
commit a04866c487
9 changed files with 116 additions and 107 deletions
@@ -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<Props> = observer(function Filter({
type ControlledProps = BaseProps & {
name?: string;
value?: string;
state?: never;
onFilter?: (value: string, name?: string) => void;
};
type ObjectsProps<TKey extends keyof TState, TState> = BaseProps & {
name: TKey;
state: TState;
onFilter?: (value: TState[TKey], name: TKey) => void;
value?: never;
};
export const Filter: React.FC<ControlledProps | ObjectsProps<any, any>> = observer(function Filter({
state,
name,
value: valueControlled,
onFilter,
toggleMode,
onToggle,
@@ -89,26 +98,34 @@ export const Filter: React.FC<Props> = observer(function Filter({
}) {
const [inputRef] = useFocus<HTMLInputElement>({});
const [toggled, setToggled] = useState(!toggleMode);
const [filterState] = useState<IFilterState>(() => 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<HTMLInputElement>) => {
if (event.key === 'Enter' || event.keyCode === 13) {
@@ -131,7 +148,11 @@ export const Filter: React.FC<Props> = 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))(
<filter-container
@@ -145,15 +166,16 @@ export const Filter: React.FC<Props> = 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 })}
/>
<IconButton
name='search'
disabled={disabled}
onClick={toggleMode ? toggle : undefined}
onClick={toggle}
{...use({ toggled })}
/>
</filter-container>
@@ -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<IConnectionFormProps>
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<IConnectionFormProps>
</Group>
) : (
<>
{(unsaved || cloud) && (
{infoItem && (
<info-item>
<IconOrImage icon={infoItem.icon} />
{infoItem.text}
{translate(infoItem.text)}
</info-item>
)}
<ConnectionAccessGrantedList
@@ -19,14 +19,13 @@ import {
BASE_CONTAINERS_STYLES,
Group,
Button,
IFilterState,
useObjectRef
} from '@cloudbeaver/core-blocks';
import { useTranslate } from '@cloudbeaver/core-localization';
import { TLocalizationToken, 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,43 +65,36 @@ export const ConnectionAccessGrantedList: React.FC<Props> = observer(function Co
const translate = useTranslate();
const [selectedSubjects] = useState<Map<any, boolean>>(() => observable(new Map()));
const [filterState] = useState<IFilterState>(() => 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)(
<Group box medium>
<ConnectionAccessTableHeader filter={filterState} disabled={disabled}>
<Button disabled={disabled || !subjectsSelected.get()} mod={['outlined']} onClick={revoke}>{translate('connections_connection_access_revoke')}</Button>
<ConnectionAccessTableHeader filterState={filterState} disabled={disabled}>
<Button disabled={disabled || !selectedList.get().length} mod={['outlined']} onClick={revoke}>{translate('connections_connection_access_revoke')}</Button>
<Button disabled={disabled} mod={['raised']} onClick={props.onEdit}>{translate('connections_connection_access_edit')}</Button>
</ConnectionAccessTableHeader>
<Table selectedItems={selectedSubjects}>
@@ -113,7 +102,7 @@ export const ConnectionAccessGrantedList: React.FC<Props> = observer(function Co
<TableBody>
<TableItem item='tableInfo'>
<TableColumnValue colSpan={5}>
{tableInfoText}
{translate(tableInfoText)}
</TableColumnValue>
</TableItem>
{roles.get().map(role => (
@@ -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<Props> = observer(function Connectio
const translate = useTranslate();
const [selectedSubjects] = useState<Map<any, boolean>>(() => observable(new Map()));
const [filterState] = useState<IFilterState>(() => 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)(
<Group box medium>
<ConnectionAccessTableHeader filter={filterState} disabled={disabled}>
<Button disabled={disabled || !subjectsSelected.get()} mod={['raised']} onClick={grant}>{translate('connections_connection_access_grant')}</Button>
<ConnectionAccessTableHeader filterState={filterState} disabled={disabled}>
<Button disabled={disabled || !selectedList.get().length} mod={['raised']} onClick={grant}>{translate('connections_connection_access_grant')}</Button>
</ConnectionAccessTableHeader>
<Table selectedItems={selectedSubjects}>
<ConnectionAccessTableInnerHeader />
@@ -116,7 +104,7 @@ export const ConnectionAccessList: React.FC<Props> = 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)}
/>
))}
@@ -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<Props> = observer(function ConnectionAccessTableHeader({ filter, disabled, className, children }) {
export const ConnectionAccessTableHeader: React.FC<Props> = observer(function ConnectionAccessTableHeader({ filterState, disabled, className, children }) {
const translate = useTranslate();
return styled(useStyles(styles))(
<header className={className}>
<Filter disabled={disabled} placeholder={translate('connections_connection_access_filter_placeholder')} state={filter} />
<Filter
disabled={disabled}
placeholder={translate('connections_connection_access_filter_placeholder')}
name='filterValue'
state={filterState}
/>
<buttons>
{children}
</buttons>
@@ -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<Props> = observer(function ConnectionAccessTableItem({
id, name, description, icon, iconTooltip, disabled, className,
}) {
const translate = useTranslate();
return styled(style)(
<TableItem
item={id}
@@ -41,7 +43,7 @@ export const ConnectionAccessTableItem: React.FC<Props> = observer(function Conn
<TableColumnValue centerContent flex>
<TableItemSelect disabled={disabled} />
</TableColumnValue>
<TableColumnValue><StaticImage icon={icon} title={iconTooltip} /></TableColumnValue>
<TableColumnValue><StaticImage icon={icon} title={translate(iconTooltip)} /></TableColumnValue>
<TableColumnValue>{name}</TableColumnValue>
<TableColumnValue>{description}</TableColumnValue>
</TableItem>
@@ -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 => {
@@ -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.'],
@@ -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', 'Администраторы видят все подключения.'],