Merge branch 'devel' into feat/user-tab

This commit is contained in:
Naumov Alexey
2021-06-09 18:45:30 +03:00
40 changed files with 939 additions and 291 deletions
@@ -77,4 +77,30 @@ public class LocalAuthProvider implements DBWAuthProvider<LocalAuthSession> {
return SecurityUtils.makeDigest(password);
}
public static boolean changeUserPassword(@NotNull WebSession webSession, @NotNull String oldPassword, @NotNull String newPassword) throws DBException {
String userName = webSession.getUser().getUserId();
WebAuthProviderDescriptor authProvider = WebServiceRegistry.getInstance().getAuthProvider(PROVIDER_ID);
Map<String, Object> storedCredentials = CBApplication.getInstance().getSecurityController().getUserCredentials(userName, authProvider);
if (storedCredentials == null) {
throw new DBException("Invalid user name or password");
}
String storedPasswordHash = CommonUtils.toString(storedCredentials.get(CRED_PASSWORD), null);
if (CommonUtils.isEmpty(storedPasswordHash)) {
throw new DBException("User has no password (login restricted)");
}
if (CommonUtils.isEmpty(oldPassword)) {
throw new DBException("No user password provided");
}
String clientPasswordHash = WebAuthProviderPropertyEncryption.hash.encrypt(userName, oldPassword);
if (!storedPasswordHash.equals(clientPasswordHash)) {
throw new DBException("Invalid user name or password");
}
storedCredentials.put(CRED_PASSWORD, clientPasswordHash);
CBApplication.getInstance().getSecurityController().setUserCredentials(userName, authProvider, storedCredentials);
return true;
}
}
@@ -385,6 +385,7 @@ public class WebSQLProcessor {
for (int i = 0; i < updateAttributes.length; i++) {
DBDAttributeBinding updateAttribute = updateAttributes[i];
Object cellRawValue = updateValues.get(String.valueOf(updateAttribute.getOrdinalPosition()));
cellRawValue = WebSQLUtils.makePlainCellValue(cellRawValue);
Object realCellValue = updateAttribute.getValueHandler().getValueFromObject(session, updateAttribute, cellRawValue, false, true);
rowValues[i] = realCellValue;
finalRow[updateAttribute.getOrdinalPosition()] = WebSQLUtils.makeWebCellValue(webSession, null, realCellValue, dataFormat);
@@ -46,6 +46,16 @@ public class WebSQLUtils {
public static final int BINARY_PREVIEW_LENGTH = 255;
public static final int BINARY_MAX_LENGTH = 1 * 1024 * 1024;
public static final String VALUE_TYPE_ATTR = "$type";
public static final String VALUE_TYPE_COLLECTION = "collection";
public static final String VALUE_TYPE_MAP = "map";
public static final String VALUE_TYPE_DOCUMENT = "document";
public static final String VALUE_TYPE_CONTENT = "content";
public static final String VALUE_TYPE_GEOMETRY = "geometry";
public static final String ATTR_TEXT = "text";
public static final String ATTR_BINARY = "binary";
public static Object makeWebCellValue(WebSession session, DBSTypedObject type, Object cellValue, WebDataFormat dataFormat) throws DBCException {
if (cellValue instanceof Date) {
return CBConstants.ISO_DATE_FORMAT.format(cellValue);
@@ -77,7 +87,7 @@ public class WebSQLUtils {
items[i] = makeWebCellValue(session, collection.getComponentType(), collection.getItem(i), dataFormat);
}
Map<String, Object> map = createMapOfType("collection");
Map<String, Object> map = createMapOfType(VALUE_TYPE_COLLECTION);
map.put("value", items);
return map;
} else if (value instanceof DBDComposite) {
@@ -87,7 +97,7 @@ public class WebSQLUtils {
struct.put(attr.getName(), makeWebCellValue(session, attr, composite.getAttributeValue(attr), dataFormat));
}
Map<String, Object> map = createMapOfType("map");
Map<String, Object> map = createMapOfType(VALUE_TYPE_MAP);
map.put("value", struct);
return map;
}
@@ -97,7 +107,7 @@ public class WebSQLUtils {
@NotNull
private static Map<String, Object> createMapOfType(String type) {
Map<String, Object> map = new LinkedHashMap<>();
map.put("$type", type);
map.put(VALUE_TYPE_ATTR, type);
return map;
}
@@ -111,7 +121,7 @@ public class WebSQLUtils {
throw new DBCException("Error serializing document", e);
}
Map<String, Object> map = createMapOfType("document");
Map<String, Object> map = createMapOfType(VALUE_TYPE_DOCUMENT);
map.put("id", CommonUtils.toString(document.getDocumentId()));
map.put("contentType", document.getDocumentContentType());
map.put("properties", Collections.emptyMap());
@@ -121,27 +131,27 @@ public class WebSQLUtils {
private static Object serializeContentValue(WebSession session, DBDContent value) throws DBCException {
Map<String, Object> map = createMapOfType("content");
Map<String, Object> map = createMapOfType(VALUE_TYPE_CONTENT);
if (ContentUtils.isTextContent(value)) {
String stringValue = ContentUtils.getContentStringValue(session.getProgressMonitor(), value);
map.put("text", stringValue);
map.put(ATTR_TEXT, stringValue);
} else {
map.put("binary", true);
map.put(ATTR_BINARY, true);
byte[] binaryValue = ContentUtils.getContentBinaryValue(session.getProgressMonitor(), value);
if (binaryValue != null) {
byte[] previewValue = binaryValue;
if (previewValue.length > BINARY_PREVIEW_LENGTH) {
previewValue = Arrays.copyOf(previewValue, BINARY_PREVIEW_LENGTH);
}
map.put("text", GeneralUtils.convertToString(binaryValue, 0, binaryValue.length));
map.put(ATTR_TEXT, GeneralUtils.convertToString(binaryValue, 0, binaryValue.length));
byte[] inlineValue = binaryValue;
if (inlineValue.length > BINARY_MAX_LENGTH) {
inlineValue = Arrays.copyOf(inlineValue, BINARY_PREVIEW_LENGTH);
}
map.put("binary", Base64.encode(inlineValue));
map.put(ATTR_BINARY, Base64.encode(inlineValue));
} else {
map.put("text", null);
map.put(ATTR_TEXT, null);
}
}
map.put("contentType", value.getContentType());
@@ -150,9 +160,9 @@ public class WebSQLUtils {
}
private static Object serializeGeometryValue(DBGeometry value) {
Map<String, Object> map = createMapOfType("geometry");
Map<String, Object> map = createMapOfType(VALUE_TYPE_GEOMETRY);
map.put("srid", value.getSRID());
map.put("text", value.toString());
map.put(ATTR_TEXT, value.toString());
map.put("properties", value.getProperties());
DBGeometry xValue = GisTransformUtils.transformToSRID(value, GisConstants.SRID_4326);
@@ -162,4 +172,23 @@ public class WebSQLUtils {
return map;
}
public static Object makePlainCellValue(Object value) throws DBCException {
if (value instanceof Map) {
Map<String, Object> map = (Map<String, Object>) value;
Object typeAttr = map.get(VALUE_TYPE_ATTR);
if (typeAttr instanceof String) {
switch ((String)typeAttr) {
case VALUE_TYPE_CONTENT:
if (map.get(ATTR_BINARY) != null) {
throw new DBCException("Binary content edit is not supported yet");
}
value = map.get(ATTR_TEXT);
break;
default:
throw new DBCException("Type '" + typeAttr + "' edit is not supported yet");
}
}
}
return value;
}
}
@@ -78,4 +78,6 @@ extend type Query {
authProviders: [AuthProviderInfo!]!
authChangeLocalPassword(oldPassword: String!, newPassword: String!): Boolean!
}
@@ -48,4 +48,6 @@ public interface DBWServiceAuth extends DBWService {
@WebAction(requirePermissions = {})
WebAuthProviderInfo[] getAuthProviders();
@WebAction()
boolean changeLocalPassword(@NotNull WebSession webSession, @NotNull String oldPassword, @NotNull String newPassword) throws DBWebException;
}
@@ -47,6 +47,11 @@ public class WebServiceBindingAuth extends WebServiceBindingBase<DBWServiceAuth>
})
.dataFetcher("activeUser", env -> getService(env).activeUser(getWebSession(env, false)))
.dataFetcher("authProviders", env -> getService(env).getAuthProviders())
.dataFetcher("authChangeLocalPassword", env -> getService(env).changeLocalPassword(
getWebSession(env),
env.getArgument("oldPassword"),
env.getArgument("newPassword")
))
;
}
@@ -248,4 +248,16 @@ public class WebServiceAuthImpl implements DBWServiceAuth {
.toArray(WebAuthProviderInfo[]::new);
}
@Override
public boolean changeLocalPassword(@NotNull WebSession webSession, @NotNull String oldPassword, @NotNull String newPassword) throws DBWebException {
if (webSession.getUser() == null) {
throw new DBWebException("User must be logged in");
}
try {
return LocalAuthProvider.changeUserPassword(webSession, oldPassword, newPassword);
} catch (DBException e) {
throw new DBWebException("Error changing user password", e);
}
}
}
+2 -2
View File
@@ -18,12 +18,12 @@
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.2.5</version>
<version>42.2.20</version>
</dependency>
<dependency>
<groupId>net.postgis</groupId>
<artifactId>postgis-jdbc</artifactId>
<version>2.2.1</version>
<version>2.5.0</version>
</dependency>
</dependencies>
@@ -0,0 +1,183 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import { useCallback, useEffect, useState } from 'react';
import styled, { css, use } from 'reshadow';
import { composes, useStyles } from '@cloudbeaver/core-theming';
import { IconButton } from '../IconButton';
import { useFocus } from '../useFocus';
import { InputFieldNew } from './InputFieldNew';
const styles = css`
filter-container {
position: relative;
min-width: 24px;
min-height: 24px;
}
InputFieldNew {
display: none;
width: 300px;
&[|toggled] {
display: block;
}
}
IconButton {
position: absolute;
right: 2px;
top: 2px;
margin: 0;
width: 24px;
height: 24px;
border-radius: 2px;
cursor: auto;
&[|toggled] {
border-radius: unset;
}
}
`;
const toggleModeButtonStyle = composes(
css`
IconButton {
composes: theme-background-primary theme-text-on-primary from global;
}
`,
css`
IconButton {
cursor: pointer;
}
`);
const innerInputStyle = css`
input {
height: 28px;
padding-right: 24px !important;
}
`;
interface BaseProps {
toggleMode?: boolean;
onToggle?: (status: boolean) => void;
placeholder?: string;
disabled?: boolean;
className?: string;
}
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,
placeholder,
disabled,
className,
}) {
const [inputRef] = useFocus<HTMLInputElement>({});
const [toggled, setToggled] = useState(!toggleMode);
const filter = useCallback((value: string | number, name?: string) => {
value = String(value).trim();
if (state && name) {
state[name] = value;
}
if (onFilter) {
onFilter(value, name);
}
}, [onFilter, state]);
const toggle = useCallback(() => {
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) {
event.stopPropagation();
}
}, []);
const handleClick = (event: React.MouseEvent<HTMLInputElement>) => {
event.stopPropagation();
};
const preventPropagation = (event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation();
event.preventDefault();
};
useEffect(() => {
if (toggled && toggleMode) {
inputRef.current?.focus();
}
}, [toggled, toggleMode, inputRef]);
let value: any = valueControlled;
if (state && name !== undefined && name in state) {
value = state[name];
}
return styled(useStyles(styles, toggleMode && toggleModeButtonStyle))(
<filter-container
as='div'
className={className}
onClick={handleClick}
onDoubleClick={preventPropagation}
>
<InputFieldNew
ref={inputRef}
style={innerInputStyle}
placeholder={placeholder}
disabled={disabled}
name={name}
value={value}
onKeyDown={onKeyDown}
onChange={filter}
{...use({ toggled })}
/>
<IconButton
name='search'
disabled={disabled}
onClick={toggle}
{...use({ toggled })}
/>
</filter-container>
);
});
@@ -1,119 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import { useCallback, useContext, useEffect, useState } from 'react';
import styled, { css, use } from 'reshadow';
import { composes, useStyles } from '@cloudbeaver/core-theming';
import { InputFieldNew } from '../../FormControls/InputFieldNew';
import { IconButton } from '../../IconButton';
import { useFocus } from '../../useFocus';
import { TreeNodeContext } from './TreeNodeContext';
const styles = composes(
css`
IconButton {
composes: theme-background-primary theme-text-on-primary from global;
}
`,
css`
InputFieldNew {
display: none;
width: 300px;
&[|filterEnabled] {
display: block;
}
}
IconButton {
position: absolute;
right: 2px;
margin: 0;
width: 24px;
height: 24px;
cursor: pointer;
border-radius: 2px;
&[|filterEnabled] {
border-radius: unset;
}
}
`);
const innerInputStyle = css`
input {
height: 28px;
padding-right: 24px !important;
}
`;
interface Props {
disabled?: boolean;
className?: string;
}
export const TreeNodeFilter: React.FC<Props> = observer(function TreeNodeFilter({
disabled,
className,
}) {
const context = useContext(TreeNodeContext);
const [inputRef] = useFocus<HTMLInputElement>({});
const [filterEnabled, setFilterEnabled] = useState(false);
if (!context) {
throw new Error('Context not provided');
}
const onFilterEnabledChange = useCallback(() => {
setFilterEnabled(prev => {
if (prev) {
context.filter('');
}
return !prev;
});
}, [context]);
const onKeyDown = useCallback((event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter' || event.keyCode === 13) {
event.stopPropagation();
}
}, []);
const handleClick = (event: React.MouseEvent<HTMLInputElement>) => {
event.stopPropagation();
};
const preventPropagation = (event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation();
event.preventDefault();
};
useEffect(() => {
if (filterEnabled) {
inputRef.current?.focus();
}
}, [filterEnabled]);
useEffect(() => () => { context.filter(''); }, []);
return styled(useStyles(styles))(
<filter-container className={className} onClick={handleClick} onDoubleClick={preventPropagation}>
<InputFieldNew
ref={inputRef}
style={innerInputStyle}
disabled={disabled}
value={context.filterValue}
{...use({ filterEnabled })}
onKeyDown={onKeyDown}
onChange={value => context.filter(String(value).trim())}
/>
<IconButton name='search' disabled={disabled} onClick={onFilterEnabledChange} {...use({ filterEnabled })} />
</filter-container>
);
});
+1 -1
View File
@@ -83,7 +83,6 @@ export * from './Tree/TreeNode/TreeNodeIcon';
export * from './Tree/TreeNode/TreeNodeName';
export * from './Tree/TreeNode/TreeNodeNested';
export * from './Tree/TreeNode/TreeNodeSelect';
export * from './Tree/TreeNode/TreeNodeFilter';
export * from './Tree/TreeNode/TreeNodeStyles';
export * from './Button';
export * from './ToolsPanel/ToolsAction';
@@ -96,6 +95,7 @@ export * from './FormControls/Checkboxes/Switch';
export * from './FormControls/Checkboxes/SwitchNew';
export * from './FormControls/Checkboxes/useCheckboxState';
export * from './FormControls/FormBox';
export * from './FormControls/Filter';
export * from './Containers/BASE_CONTAINERS_STYLES';
export * from './Containers/Container';
export * from './Containers/Group';
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 53 (72520) - https://sketchapp.com -->
<title>group</title>
<desc>Created with Sketch.</desc>
<defs>
<polygon id="path-1" points="0 22.208 0 0.0003 29.0417 0.0003 29.0417 22.208"></polygon>
</defs>
<g id="group" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Page-1" transform="translate(1.000000, 4.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<g id="Clip-2"></g>
<path d="M18.7917,10.25 C21.6217,10.25 23.9167,7.955 23.9167,5.125 C23.9167,2.295 21.6217,0 18.7917,0 C18.2447,0 17.7157,0.085 17.2197,0.239 C18.1937,1.623 18.7917,3.314 18.7917,5.125 C18.7917,6.936 18.1937,8.61 17.2197,9.994 C17.7157,10.165 18.2447,10.25 18.7917,10.25 M10.2497,10.25 C13.0807,10.25 15.3747,7.955 15.3747,5.125 C15.3747,2.295 13.0807,0 10.2497,0 C7.4197,0 5.1247,2.295 5.1247,5.125 C5.1247,7.955 7.4197,10.25 10.2497,10.25 M21.5597,13.94 C22.9767,15.17 23.9167,16.776 23.9167,18.792 L23.9167,22.208 L29.0417,22.208 L29.0417,18.792 C29.0417,16.161 24.9927,14.521 21.5597,13.94 M10.2497,13.667 C6.8337,13.667 -0.0003,15.375 -0.0003,18.792 L-0.0003,22.208 L20.4997,22.208 L20.4997,18.792 C20.4997,15.375 13.6667,13.667 10.2497,13.667" id="Fill-1" fill="#338ECC" mask="url(#mask-2)"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 53 (72520) - https://sketchapp.com -->
<title>user</title>
<desc>Created with Sketch.</desc>
<g id="user" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M15.8,5.2779 C17.801,5.2779 19.422,6.8999 19.422,8.8999 C19.422,10.9009 17.801,12.5229 15.8,12.5229 L15.8,12.5229 C13.799,12.5229 12.178,10.9009 12.178,8.8999 L12.178,8.8999 C12.178,6.8999 13.799,5.2779 15.8,5.2779 Z M15.8,20.8029 C20.923,20.8029 26.322,23.3209 26.322,24.4249 L26.322,26.3229 L5.278,26.3229 L5.278,24.4249 C5.278,23.3209 10.625,20.8029 15.8,20.8029 L15.8,20.8029 Z M15.8,2.0009 C11.989,2.0009 8.9,5.0899 8.9,8.8999 C8.9,12.7109 11.989,15.7999 15.8,15.7999 C19.611,15.7999 22.7,12.7109 22.7,8.8999 C22.7,5.0899 19.611,2.0009 15.8,2.0009 L15.8,2.0009 Z M15.8,17.5249 C11.194,17.5249 2,19.8199 2,24.4249 L2,29.5999 L29.6,29.5999 L29.6,24.4249 C29.6,19.8199 20.406,17.5249 15.8,17.5249 L15.8,17.5249 Z" id="Page-1" fill="#338ECC"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -6,18 +6,13 @@
* you may not use this file except in compliance with the License.
*/
import { computed } from 'mobx';
import { observer } from 'mobx-react-lite';
import { useMemo } from 'react';
import styled, { css } from 'reshadow';
import { RolesResource, UsersResource } from '@cloudbeaver/core-authentication';
import {
Table,
TableHeader,
TableColumnHeader,
TableBody,
TableItem,
TableColumnValue,
TableItemSelect,
TextPlaceholder,
Loader,
useTab,
@@ -25,43 +20,56 @@ import {
useMapResource,
BASE_CONTAINERS_STYLES,
ColoredContainer,
Group
Group,
IconOrImage,
} from '@cloudbeaver/core-blocks';
import { useTranslate } from '@cloudbeaver/core-localization';
import { useStyles, composes } from '@cloudbeaver/core-theming';
import { TLocalizationToken, useTranslate } from '@cloudbeaver/core-localization';
import { useStyles } from '@cloudbeaver/core-theming';
import { isCloudConnection } from '../../Administration/ConnectionsResource';
import type { IConnectionFormProps } from '../IConnectionFormProps';
import { ConnectionAccessGrantedList } from './ConnectionAccessGrantedList';
import { ConnectionAccessList } from './ConnectionAccessList';
import { useConnectionAccessState } from './useConnectionAccessState';
const styles = composes(
css`
Table {
composes: theme-background-surface theme-text-on-surface from global;
}
`,
css`
ColoredContainer {
flex: 1;
overflow: auto;
}
Group {
max-height: 100%;
position: relative;
overflow: auto !important;
}
Table {
flex: 1;
}
`
);
const styles = css`
ColoredContainer {
flex: 1;
overflow: auto;
}
Group {
max-height: 100%;
position: relative;
overflow: auto !important;
}
info-item {
display: flex;
align-items: center;
}
IconOrImage {
width: 24px;
height: 24px;
margin-right: 16px;
}
Loader {
z-index: 2;
}
`;
interface IInfoItem {
text: TLocalizationToken;
icon: string;
}
export const ConnectionAccess: TabContainerPanelComponent<IConnectionFormProps> = observer(function ConnectionAccess({
tabId,
state: formState,
}) {
const { state, load, select } = useConnectionAccessState(formState);
const { state, edit, grant, revoke, load } = useConnectionAccessState(formState.info);
const style = useStyles(styles, BASE_CONTAINERS_STYLES);
const translate = useTranslate();
const unsaved = state.initialGrantedSubjects.length !== state.grantedSubjects.length
|| state.initialGrantedSubjects.some(subject => !state.grantedSubjects.includes(subject));
const users = useMapResource(UsersResource, null, {
onLoad: resource => resource.loadAll(),
@@ -71,56 +79,74 @@ export const ConnectionAccess: TabContainerPanelComponent<IConnectionFormProps>
onLoad: resource => resource.loadAll(),
});
const grantedUsers = useMemo(() => computed(() => users.resource.values
.filter(user => state.grantedSubjects.includes(user.userId))
), [state.grantedSubjects, users.resource]);
const grantedRoles = useMemo(() => computed(() => roles.resource.values
.filter(role => state.grantedSubjects.includes(role.roleId))
), [state.grantedSubjects, roles.resource]);
const { selected } = useTab(tabId, load);
const loading = users.isLoading() || roles.isLoading() || state.loading;
const disabled = loading || !state.loaded || formState.disabled;
const cloud = formState.info ? isCloudConnection(formState.info) : false;
const disabled = loading || !state.loaded || formState.disabled || cloud;
let infoItem: IInfoItem | null = null;
if (unsaved) {
infoItem = {
text: 'connections_connection_access_save_reminder',
icon: '/icons/info_icon.svg',
};
}
if (cloud) {
infoItem = {
text: 'connections_connection_access_cloud_placeholder',
icon: '/icons/info_icon.svg',
};
}
if (!selected) {
return null;
}
if (users.resource.values.length === 0 && roles.resource.values.length) {
return styled(style)(
<ColoredContainer parent>
<Group keepSize large>
<TextPlaceholder>{translate('connections_administration_connection_access_empty')}</TextPlaceholder>
</Group>
</ColoredContainer>
);
}
return styled(style)(
<ColoredContainer parent>
<Group box keepSize large>
<Table selectedItems={state.selectedSubjects} onSelect={select}>
<TableHeader>
<TableColumnHeader min />
<TableColumnHeader>{translate('connections_connection_name')}</TableColumnHeader>
<TableColumnHeader />
</TableHeader>
<TableBody>
{roles.resource.values.map(role => (
<TableItem key={role.roleId} item={role.roleId} selectDisabled={disabled}>
<TableColumnValue centerContent flex>
<TableItemSelect disabled={disabled} />
</TableColumnValue>
<TableColumnValue>{role.roleName}</TableColumnValue>
<TableColumnValue />
</TableItem>
))}
{users.resource.values.map(user => (
<TableItem key={user.userId} item={user.userId} selectDisabled={disabled}>
<TableColumnValue centerContent flex>
<TableItemSelect disabled={disabled} />
</TableColumnValue>
<TableColumnValue>{user.userId}</TableColumnValue>
<TableColumnValue />
</TableItem>
))}
</TableBody>
</Table>
<Loader key="overlay" loading={loading} overlay />
</Group>
</ColoredContainer>
<Loader state={[users, roles, state]}>
{() => styled(style)(
<ColoredContainer parent gap>
{!users.resource.values.length && !roles.resource.values.length ? (
<Group keepSize large>
<TextPlaceholder>{translate('connections_administration_connection_access_empty')}</TextPlaceholder>
</Group>
) : (
<>
{infoItem && (
<info-item>
<IconOrImage icon={infoItem.icon} />
{translate(infoItem.text)}
</info-item>
)}
<ConnectionAccessGrantedList
grantedUsers={grantedUsers.get()}
grantedRoles={grantedRoles.get()}
disabled={disabled}
onEdit={edit}
onRevoke={revoke}
/>
{state.editing && (
<ConnectionAccessList
userList={users.resource.values}
roleList={roles.resource.values}
grantedSubjects={state.grantedSubjects}
disabled={disabled}
onGrant={grant}
/>
)}
</>
)}
</ColoredContainer>
)}
</Loader>
);
});
@@ -0,0 +1,133 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { computed, observable } from 'mobx';
import { observer } from 'mobx-react-lite';
import { useCallback, useMemo, useState } from 'react';
import styled, { css } from 'reshadow';
import {
Table,
TableBody,
TableItem,
TableColumnValue,
BASE_CONTAINERS_STYLES,
Group,
Button,
useObjectRef
} from '@cloudbeaver/core-blocks';
import { TLocalizationToken, useTranslate } from '@cloudbeaver/core-localization';
import type { AdminRoleInfo, AdminUserInfoFragment } from '@cloudbeaver/core-sdk';
import { useStyles, composes } from '@cloudbeaver/core-theming';
import { ConnectionAccessTableHeader, IFilterState } from './ConnectionAccessTableHeader/ConnectionAccessTableHeader';
import { ConnectionAccessTableInnerHeader } from './ConnectionAccessTableHeader/ConnectionAccessTableInnerHeader';
import { ConnectionAccessTableItem } from './ConnectionAccessTableitem';
import { getFilteredRoles, getFilteredUsers } from './getFilteredSubjects';
const styles = composes(
css`
Table {
composes: theme-background-surface theme-text-on-surface from global;
}
`,
css`
Group {
height: 100%;
position: relative;
overflow: auto !important;
}
`
);
interface Props {
grantedUsers: AdminUserInfoFragment[];
grantedRoles: AdminRoleInfo[];
disabled: boolean;
onRevoke: (subjectIds: string[]) => void;
onEdit: () => void;
}
export const ConnectionAccessGrantedList: React.FC<Props> = observer(function ConnectionAccessGrantedList({
grantedUsers,
grantedRoles,
disabled,
onRevoke,
onEdit,
}) {
const props = useObjectRef({ onRevoke, onEdit });
const style = useStyles(styles, BASE_CONTAINERS_STYLES);
const translate = useTranslate();
const [selectedSubjects] = useState<Map<any, boolean>>(() => observable(new Map()));
const [filterState] = useState<IFilterState>(() => observable({ filterValue: '' }));
const selectedList = useMemo(() => computed(
() => Array.from(selectedSubjects.entries()).filter(([key, value]) => value).map(([key]) => key)
), [selectedSubjects]);
const revoke = useCallback(() => {
props.onRevoke(selectedList.get());
selectedSubjects.clear();
}, []);
const roles = useMemo(() => computed(() => getFilteredRoles(
grantedRoles, filterState.filterValue
)), [filterState, grantedRoles]);
const users = useMemo(() => computed(() => getFilteredUsers(
grantedUsers, filterState.filterValue
)), [filterState, grantedUsers]);
let tableInfoText: TLocalizationToken = 'connections_connection_access_admin_info';
if (!roles.get().length && !users.get().length) {
if (filterState.filterValue) {
tableInfoText = 'connections_connection_access_filter_no_result';
} else {
tableInfoText = 'connections_connection_access_empty_table_placeholder';
}
}
return styled(style)(
<Group box medium>
<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}>
<ConnectionAccessTableInnerHeader />
<TableBody>
<TableItem item='tableInfo'>
<TableColumnValue colSpan={5}>
{translate(tableInfoText)}
</TableColumnValue>
</TableItem>
{roles.get().map(role => (
<ConnectionAccessTableItem
key={role.roleId}
id={role.roleId}
name={role.roleName || ''}
description={role.description}
icon='/icons/role.svg'
iconTooltip={translate('connections_connection_access_role_tooltip')}
disabled={disabled}
/>
))}
{users.get().map(user => (
<ConnectionAccessTableItem
key={user.userId}
id={user.userId}
name={user.userId}
icon='/icons/user.svg'
iconTooltip={translate('connections_connection_access_user_tooltip')}
disabled={disabled}
/>
))}
</TableBody>
</Table>
</Group>
);
});
@@ -0,0 +1,125 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { computed, observable } from 'mobx';
import { observer } from 'mobx-react-lite';
import { useCallback, useMemo, useState } from 'react';
import styled, { css } from 'reshadow';
import {
Table,
TableBody,
TableItem,
TableColumnValue,
BASE_CONTAINERS_STYLES,
Group,
Button,
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, IFilterState } from './ConnectionAccessTableHeader/ConnectionAccessTableHeader';
import { ConnectionAccessTableInnerHeader } from './ConnectionAccessTableHeader/ConnectionAccessTableInnerHeader';
import { ConnectionAccessTableItem } from './ConnectionAccessTableitem';
import { getFilteredRoles, getFilteredUsers } from './getFilteredSubjects';
const styles = composes(
css`
Table {
composes: theme-background-surface theme-text-on-surface from global;
}
`,
css`
Group {
height: 100%;
position: relative;
overflow: auto !important;
}
`
);
interface Props {
userList: AdminUserInfoFragment[];
roleList: AdminRoleInfo[];
grantedSubjects: string[];
onGrant: (subjectIds: string[]) => void;
disabled: boolean;
}
export const ConnectionAccessList: React.FC<Props> = observer(function ConnectionAccessList({
userList,
roleList,
grantedSubjects,
onGrant,
disabled,
}) {
const props = useObjectRef({ onGrant });
const style = useStyles(styles, BASE_CONTAINERS_STYLES);
const translate = useTranslate();
const [selectedSubjects] = useState<Map<any, boolean>>(() => observable(new Map()));
const [filterState] = useState<IFilterState>(() => observable({ filterValue: '' }));
const selectedList = useMemo(() => computed(
() => Array.from(selectedSubjects.entries()).filter(([key, value]) => value).map(([key]) => key)
), [selectedSubjects]);
const grant = useCallback(() => {
props.onGrant(selectedList.get());
selectedSubjects.clear();
}, []);
const roles = useMemo(() => computed(() => getFilteredRoles(
roleList, filterState.filterValue
)), [filterState, roleList]);
const users = useMemo(() => computed(() => getFilteredUsers(
userList, filterState.filterValue
)), [filterState, userList]);
return styled(style)(
<Group box medium>
<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 />
<TableBody>
{!roles.get().length && !users.get().length && filterState.filterValue && (
<TableItem item='tableInfo'>
<TableColumnValue colSpan={5}>
{translate('connections_connection_access_filter_no_result')}
</TableColumnValue>
</TableItem>
)}
{roles.get().map(role => (
<ConnectionAccessTableItem
key={role.roleId}
id={role.roleId}
name={role.roleName || ''}
description={role.description}
icon='/icons/role.svg'
iconTooltip='connections_connection_access_role_tooltip'
disabled={disabled || grantedSubjects.includes(role.roleId)}
/>
))}
{users.get().map(user => (
<ConnectionAccessTableItem
key={user.userId}
id={user.userId}
name={user.userId}
icon='/icons/user.svg'
iconTooltip={translate('connections_connection_access_user_tooltip')}
disabled={disabled || grantedSubjects.includes(user.userId)}
/>
))}
</TableBody>
</Table>
</Group>
);
});
@@ -9,7 +9,6 @@
import { AdministrationScreenService } from '@cloudbeaver/core-administration';
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import type { IExecutionContextProvider } from '@cloudbeaver/core-executor';
import type { AdminConnectionGrantInfo } from '@cloudbeaver/core-sdk';
import type { MetadataValueGetter } from '@cloudbeaver/core-utils';
import { ConnectionsResource } from '../../Administration/ConnectionsResource';
@@ -57,10 +56,11 @@ export class ConnectionAccessTabService extends Bootstrap {
private stateGetter(context: IConnectionFormProps): MetadataValueGetter<string, IConnectionAccessTabState> {
return () => ({
selectedSubjects: new Map(),
loading: false,
loaded: false,
editing: false,
grantedSubjects: [],
initialGrantedSubjects: [],
});
}
@@ -93,8 +93,9 @@ export class ConnectionAccessTabService extends Bootstrap {
if (changed) {
await this.connectionsResource.setAccessSubjects(
config.connectionId,
state.grantedSubjects.map(subject => subject.subjectId)
state.grantedSubjects
);
state.initialGrantedSubjects = state.grantedSubjects.slice();
}
}
@@ -125,12 +126,12 @@ export class ConnectionAccessTabService extends Bootstrap {
}
}
private async isChanged(connectionId: string, next: AdminConnectionGrantInfo[]): Promise<boolean> {
private async isChanged(connectionId: string, next: string[]): Promise<boolean> {
const current = await this.connectionsResource.loadAccessSubjects(connectionId);
if (current.length !== next.length) {
return true;
}
return current.some(value => !next.some(subject => subject.subjectId === value.subjectId));
return current.some(value => !next.some(subjectId => subjectId === value.subjectId));
}
}
@@ -0,0 +1,67 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import styled, { css } from 'reshadow';
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 {
filterState: IFilterState;
disabled: boolean;
className?: string;
}
const styles = composes(
css`
header {
composes: theme-border-color-background theme-background-surface theme-text-on-surface from global;
}
`,
css`
buttons {
display: flex;
gap: 16px;
}
header {
overflow: hidden;
position: sticky;
top: 0;
z-index: 1;
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
gap: 16px;
border-bottom: 1px solid;
}
`
);
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')}
name='filterValue'
state={filterState}
/>
<buttons>
{children}
</buttons>
</header>
);
});
@@ -0,0 +1,28 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import { TableColumnHeader, TableHeader } from '@cloudbeaver/core-blocks';
import { useTranslate } from '@cloudbeaver/core-localization';
interface Props {
className?: string;
}
export const ConnectionAccessTableInnerHeader: React.FC<Props> = observer(function ConnectionAccessTableInnerHeader({ className }) {
const translate = useTranslate();
return (
<TableHeader className={className}>
<TableColumnHeader min />
<TableColumnHeader min />
<TableColumnHeader>{translate('connections_connection_access_user_or_role_name')}</TableColumnHeader>
<TableColumnHeader>{translate('connections_connection_description')}</TableColumnHeader>
</TableHeader>
);
});
@@ -0,0 +1,51 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
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?: TLocalizationToken;
description?: string;
className?: string;
}
const style = css`
StaticImage {
display: flex;
width: 24px;
}
`;
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}
disabled={disabled}
selectDisabled={disabled}
className={className}
>
<TableColumnValue centerContent flex>
<TableItemSelect disabled={disabled} />
</TableColumnValue>
<TableColumnValue><StaticImage icon={icon} title={translate(iconTooltip)} /></TableColumnValue>
<TableColumnValue>{name}</TableColumnValue>
<TableColumnValue>{description}</TableColumnValue>
</TableItem>
);
});
@@ -6,11 +6,10 @@
* you may not use this file except in compliance with the License.
*/
import type { AdminConnectionGrantInfo } from '@cloudbeaver/core-sdk';
export interface IConnectionAccessTabState {
loading: boolean;
loaded: boolean;
selectedSubjects: Map<string, boolean>;
grantedSubjects: AdminConnectionGrantInfo[];
grantedSubjects: string[];
initialGrantedSubjects: string[];
editing: boolean;
}
@@ -0,0 +1,29 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import type { AdminRoleInfo, AdminUserInfoFragment } from '@cloudbeaver/core-sdk';
/**
* @param {AdminUserInfoFragment[]} users
* @param {string} filter
*/
export function getFilteredUsers(users: AdminUserInfoFragment[], filter: string): AdminUserInfoFragment[] {
return users
.filter(user => user.userId.toLowerCase().includes(filter.toLowerCase()))
.sort((a, b) => (a.userId).localeCompare(b.userId));
}
/**
* @param {AdminRoleInfo[]} roles
* @param {string} filter
*/
export function getFilteredRoles(roles: AdminRoleInfo[], filter: string): AdminRoleInfo[] {
return roles
.filter(role => role.roleName?.toLowerCase().includes(filter.toLowerCase()) && role.roleId !== 'admin')
.sort((a, b) => (a.roleName ?? '').localeCompare(b.roleName ?? ''));
}
@@ -9,37 +9,34 @@
import { useObjectRef, useTabState } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { AdminSubjectType } from '@cloudbeaver/core-sdk';
import type { DatabaseConnectionFragment } from '@cloudbeaver/core-sdk';
import { ConnectionsResource } from '../../Administration/ConnectionsResource';
import type { IConnectionFormState } from '../IConnectionFormProps';
import type { IConnectionAccessTabState } from './IConnectionAccessTabState';
interface IConnectionAccessState {
state: IConnectionAccessTabState;
select: (subjectId: string, value: boolean) => void;
revoke: (subjectIds: string[]) => void;
grant: (subjectIds: string[]) => void;
edit: () => void;
load: () => Promise<void>;
}
export function useConnectionAccessState(formState: IConnectionFormState): IConnectionAccessState {
export function useConnectionAccessState(connection: DatabaseConnectionFragment | undefined): IConnectionAccessState {
const connectionsResource = useService(ConnectionsResource);
const notificationService = useService(NotificationService);
const state = useTabState<IConnectionAccessTabState>();
const select = (subjectId: string, value: boolean): void => {
if (!value) {
const index = state.grantedSubjects.findIndex(subject => subject.subjectId === subjectId);
if (index > -1) {
state.grantedSubjects.splice(index, 1);
}
return;
}
const edit = () => {
state.editing = !state.editing;
};
state.grantedSubjects.push({
connectionId: '',
subjectId,
subjectType: AdminSubjectType.User,
});
const revoke = (subjectIds: string[]): void => {
state.grantedSubjects = state.grantedSubjects.filter(subject => !subjectIds.includes(subject));
};
const grant = (subjectIds: string[]): void => {
state.grantedSubjects.push(...subjectIds);
};
const load = async () => {
@@ -50,13 +47,12 @@ export function useConnectionAccessState(formState: IConnectionFormState): IConn
try {
state.loading = true;
if (formState.info) {
state.grantedSubjects = await connectionsResource.loadAccessSubjects(formState.info.id);
if (connection) {
const grantedSubjects = await connectionsResource.loadAccessSubjects(connection.id);
state.grantedSubjects = grantedSubjects.map(subject => subject.subjectId);
state.initialGrantedSubjects = state.grantedSubjects.slice();
}
for (const subject of state.grantedSubjects) {
state.selectedSubjects.set(subject.subjectId, true);
}
state.loaded = true;
} catch (exception) {
notificationService.logException(exception, 'connections_connection_edit_access_load_failed');
@@ -64,5 +60,5 @@ export function useConnectionAccessState(formState: IConnectionFormState): IConn
state.loading = false;
};
return useObjectRef({ state, select, load });
return useObjectRef({ state, revoke, grant, edit, load });
}
@@ -31,6 +31,18 @@ export default [
['connections_connection_edit_search_hosts', 'Host names'],
['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_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 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.'],
['connections_connection_access_admin_info', 'Administators see all connections.'],
['connections_connection_description', 'Description'],
['connections_connection_driver', 'Driver'],
['connections_connection_host', 'Host'],
@@ -28,6 +28,18 @@ export default [
['connections_connection_edit_search_hosts', 'Названия хостов'],
['connections_connection_address', 'Адрес'],
['connections_connection_name', 'Название подключения'],
['connections_connection_access_user_or_role_name', 'Имя пользователя или роли'],
['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_edit', 'Изменить'],
['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', 'Администраторы видят все подключения.'],
['connections_connection_description', 'Описание'],
['connections_connection_driver', 'Драйвер'],
['connections_connection_host', 'Хост'],
@@ -2,7 +2,7 @@ query getRolesList($roleId: ID) {
roles: listRoles(roleId: $roleId) {
roleId
roleName
description
#rolePermissions
}
}
+13 -2
View File
@@ -68,6 +68,7 @@ export interface Query {
sqlSupportedOperations: DataTypeLogicalOperation[];
templateConnections: ConnectionInfo[];
updateConnectionConfiguration: ConnectionInfo;
updateRole: AdminRoleInfo;
userConnections: ConnectionInfo[];
}
@@ -108,6 +109,8 @@ export interface QueryCreateConnectionConfigurationArgs {
export interface QueryCreateRoleArgs {
roleId: Scalars['ID'];
roleName?: Maybe<Scalars['String']>;
description?: Maybe<Scalars['String']>;
}
export interface QueryCreateUserArgs {
@@ -260,6 +263,12 @@ export interface QueryUpdateConnectionConfigurationArgs {
config: ConnectionConfig;
}
export interface QueryUpdateRoleArgs {
roleId: Scalars['ID'];
roleName?: Maybe<Scalars['String']>;
description?: Maybe<Scalars['String']>;
}
export interface QueryUserConnectionsArgs {
id?: Maybe<Scalars['ID']>;
}
@@ -893,6 +902,7 @@ export interface AdminUserInfo {
export interface AdminRoleInfo {
roleId: Scalars['ID'];
roleName?: Maybe<Scalars['String']>;
description?: Maybe<Scalars['String']>;
rolePermissions: Array<Maybe<Scalars['ID']>>;
}
@@ -1042,7 +1052,7 @@ export type GetRolesListQueryVariables = Exact<{
roleId?: Maybe<Scalars['ID']>;
}>;
export interface GetRolesListQuery { roles: Array<Maybe<Pick<AdminRoleInfo, 'roleId' | 'roleName'>>> }
export interface GetRolesListQuery { roles: Array<Maybe<Pick<AdminRoleInfo, 'roleId' | 'roleName' | 'description'>>> }
export type GetUserGrantedConnectionsQueryVariables = Exact<{
userId?: Maybe<Scalars['ID']>;
@@ -1445,7 +1455,7 @@ export interface GetSqlExecuteTaskResultsMutation {
& { resultSet?: Maybe<(
Pick<SqlResultSet, 'id' | 'rows' | 'hasMoreData'>
& { columns?: Maybe<Array<Maybe<(
Pick<SqlResultColumn, 'dataKind' | 'entityName' | 'fullTypeName' | 'icon' | 'label' | 'maxLength' | 'name' | 'position' | 'precision' | 'readOnly' | 'scale' | 'typeName'>
Pick<SqlResultColumn, 'dataKind' | 'entityName' | 'fullTypeName' | 'icon' | 'label' | 'maxLength' | 'name' | 'position' | 'precision' | 'readOnly' | 'readOnlyStatus' | 'scale' | 'typeName'>
& { supportedOperations: Array<Pick<DataTypeLogicalOperation, 'id' | 'expression' | 'argumentCount'>> }
)>>>; }
)>; }
@@ -1923,6 +1933,7 @@ export const GetRolesListDocument = `
roles: listRoles(roleId: $roleId) {
roleId
roleName
description
}
}
`;
@@ -87,22 +87,9 @@ export const CellEditor = observer<Pick<EditorProps<any, any>, 'rowIdx' | 'row'
}
});
const value = formatter?.get(row[column.key]);
const numericCell = typeof tableDataContext?.getCellValue(rowIdx, column.key) === 'number';
const value = formatter?.getText(row[column.key]) ?? '';
const handleSave = () => {
if (numericCell) {
const editor = dataGridContext.model.source.getEditor(dataGridContext.resultIndex);
let value = editor.getCell(rowIdx, Number(column.key));
if (typeof value === 'string') {
value = value.replace(',', '.');
}
editor.setCell(rowIdx, Number(column.key), Number(value));
}
onClose(false);
};
const handleSave = () => onClose(false);
const handleReject = () => {
dataGridContext.model.source.getEditor(dataGridContext.resultIndex)
.revertCell(rowIdx, Number(column.key));
@@ -40,7 +40,7 @@ export const CellRenderer: React.FC<CellRendererProps<any>> = observer(function
classes.push('rdg-cell-custom-editing');
}
if (editor?.isCellEdited(rowIdx, Number(column.key))) {
if (!tableDataContext?.isIndexColumn(column.key) && editor?.isCellEdited(rowIdx, Number(column.key))) {
classes.push('rdg-cell-custom-edited');
}
@@ -90,7 +90,7 @@ export class DataGridContextMenuFilterService {
},
titleGetter() {
const val = typeof value === 'function' ? value() : value;
const stringifyValue = format.toString(val);
const stringifyValue = format.toDisplayString(val);
const wrappedValue = wrapOperationArgument(operation.id, stringifyValue);
const clippeddValue = replaceMiddle(wrappedValue, ' ... ', 8, 30);
return `${columnLabel} ${operation.expression} ${clippeddValue}`;
@@ -244,7 +244,7 @@ export class DataGridContextMenuFilterService {
icon: '/icons/filter_custom.png',
onClick: async () => {
const isNull = format.isNull(cellValue);
const stringifyCellValue = format.toString(cellValue);
const stringifyCellValue = format.toDisplayString(cellValue);
const customValue = await this.commonDialogService.open(
FilterCustomValueDialog,
{
@@ -36,7 +36,7 @@ export const CellFormatter: React.FC<FormatterProps> = observer(function CellFor
const rawValue = formatter?.get(row[column.key]) ?? row[column.key];
const classes = getClasses(rawValue);
const [menuVisible, setMenuVisible] = useState(false);
const value = formatter?.toString(rawValue) ?? String(rawValue);
const value = formatter?.toDisplayString(rawValue) ?? String(rawValue);
const handleClose = useCallback(() => {
editingContext?.closeEditor({ idx: column.idx, rowIdx });
@@ -9,9 +9,5 @@
import type { FormatterProps } from 'react-data-grid';
export const IndexFormatter: React.FC<FormatterProps> = function IndexFormatter({ rowIdx }) {
return (
<index-formatter as='div'>
{rowIdx + 1}
</index-formatter>
);
return <div>{rowIdx + 1}</div>;
};
@@ -12,7 +12,5 @@ import { Row } from 'react-data-grid';
import { CellRenderer } from '../CellRenderer/CellRenderer';
export const RowRenderer: React.FC<RowRendererProps<any>> = function RowRenderer(props) {
return (
<Row cellRenderer={CellRenderer} {...props} />
);
return <Row cellRenderer={CellRenderer} {...props} />;
};
@@ -48,7 +48,7 @@ function copyGridSelectedDataToClipboard(
if (colIndexes.includes(columnIdx)) {
const cell = editor.getCell(rowIdx, columnIdx);
const cellValue = format.get(cell);
const cellValue = format.getText(cell);
rowCellsValues.push(cellValue ?? '');
} else {
rowCellsValues.push('');
@@ -15,5 +15,5 @@ export interface IDatabaseDataFormatAction<TKey, TResult extends IDatabaseDataRe
get: (value: any) => any;
getText: (value: any) => string | null;
isNull: (value: any) => boolean;
toString: (value: any) => string;
toDisplayString: (value: any) => string;
}
@@ -11,7 +11,7 @@ import type { IExecutor } from '@cloudbeaver/core-executor';
import type { IDatabaseDataAction } from '../IDatabaseDataAction';
import type { IDatabaseDataResult } from '../IDatabaseDataResult';
export type DatabaseDataEditorActionsData<TKey> = {
export type DatabaseDataSelectActionsData<TKey> = {
type: 'select';
resultId: string;
key: TKey;
@@ -23,12 +23,11 @@ export type DatabaseDataEditorActionsData<TKey> = {
export interface IDatabaseDataSelectAction<TKey, TResult extends IDatabaseDataResult>
extends IDatabaseDataAction<any, TResult> {
readonly actions: IExecutor<DatabaseDataEditorActionsData<TKey>>;
readonly actions: IExecutor<DatabaseDataSelectActionsData<TKey>>;
isSelected: () => boolean;
isElementSelected: (key: TKey) => boolean;
getFocusedElement: () => TKey | null;
getSelectedElements: () => TKey[];
getRowSelection: (row: number) => number[];
set: (key: TKey, selected: boolean) => void;
focus: (key: TKey | null) => void;
clear: () => void;
@@ -16,6 +16,8 @@ import type { IResultSetElementKey } from './IResultSetElementKey';
import { isResultSetContentValue } from './isResultSetContentValue';
import { ResultSetDataAction } from './ResultSetDataAction';
export type IResultSetValue = string | number | Record<string, string | number | Record<string, any> | null> | null;
@databaseDataAction()
export class ResultSetFormatAction extends DatabaseDataAction<any, IDatabaseResultSet>
implements IDatabaseDataFormatAction<IResultSetElementKey, IDatabaseResultSet> {
@@ -45,11 +47,11 @@ export class ResultSetFormatAction extends DatabaseDataAction<any, IDatabaseResu
return columnReadonly || cellReadonly;
}
isNull(value: any): boolean {
isNull(value: IResultSetValue): boolean {
return this.get(value) === null;
}
get(value: any): any {
get(value: IResultSetValue): IResultSetValue {
if (value !== null && typeof value === 'object') {
if ('text' in value) {
return value.text;
@@ -62,16 +64,22 @@ export class ResultSetFormatAction extends DatabaseDataAction<any, IDatabaseResu
return value;
}
getText(value: any): string {
getText(value: IResultSetValue): string | null {
value = this.get(value);
if (value !== null && typeof value === 'object') {
return JSON.stringify(value);
}
if (typeof value === 'number') {
return String(value);
}
return value;
}
toString(value: any): string {
value = this.getText(this.get(value));
toDisplayString(value: IResultSetValue): string {
value = this.getText(value);
if (typeof value === 'string' && value.length > 1000) {
return value.split('').map(v => (v.charCodeAt(0) < 32 ? ' ' : v)).join('');
@@ -15,7 +15,7 @@ import { DatabaseDataAction } from '../../DatabaseDataAction';
import type { IDatabaseDataSource } from '../../IDatabaseDataSource';
import type { IDatabaseResultSet } from '../../IDatabaseResultSet';
import { databaseDataAction } from '../DatabaseDataActionDecorator';
import type { DatabaseDataEditorActionsData, IDatabaseDataSelectAction } from '../IDatabaseDataSelectAction';
import type { DatabaseDataSelectActionsData, IDatabaseDataSelectAction } from '../IDatabaseDataSelectAction';
import type { IResultSetElementKey } from './IResultSetElementKey';
@databaseDataAction()
@@ -23,7 +23,7 @@ export class ResultSetSelectAction extends DatabaseDataAction<any, IDatabaseResu
implements IDatabaseDataSelectAction<IResultSetElementKey, IDatabaseResultSet> {
static dataFormat = ResultDataFormat.Resultset;
readonly actions: IExecutor<DatabaseDataEditorActionsData<IResultSetElementKey>>;
readonly actions: IExecutor<DatabaseDataSelectActionsData<IResultSetElementKey>>;
readonly selectedElements: Map<number, number[]>;
private focusedElement: IResultSetElementKey | null;
@@ -292,8 +292,11 @@ export class DatabaseDataEditor<TResult extends IDatabaseDataResult> implements
}
private compareCellValue(valueA: any, valueB: any) {
valueA = valueA ?? '';
valueB = valueB ?? '';
if (typeof valueA === 'number' || typeof valueB === 'number') {
return Number(valueA) === Number(valueB);
return String(valueA) === String(valueB);
}
if (isResultSetContentValue(valueA) && isResultSetContentValue(valueB)) {
@@ -89,14 +89,14 @@ export const TextValuePresentation: TabContainerPanelComponent<IDataValuePanelPr
currentContentType: observable,
lastContentType: observable,
}, ['setContentType', 'setDefaultContentType']);
const result = model.getResult(resultIndex);
const selection = model.source.getAction(resultIndex, ResultSetSelectAction);
const selectedCells = selection.getSelectedElements();
const focusCell = selection.getFocusedElement();
let value: any;
let stringValue: string | undefined;
let stringValue = '';
let contentType = 'text/plain';
let firstSelectedCell: Required<IResultSetElementKey> | undefined;
let readonly = true;
@@ -106,11 +106,11 @@ export const TextValuePresentation: TabContainerPanelComponent<IDataValuePanelPr
firstSelectedCell = selectedCells[0] || focusCell;
value = model.source
const value = model.source
.getEditor(resultIndex)
.getCell(firstSelectedCell.row, firstSelectedCell.column);
stringValue = format.getText(format.get(value));
stringValue = format.getText(value) ?? '';
readonly = format.isReadOnly(firstSelectedCell);
if (isResultSetContentValue(value)) {
@@ -159,14 +159,14 @@ export const TextValuePresentation: TabContainerPanelComponent<IDataValuePanelPr
{useCodeEditor ? (
<CodeEditorLoader
readonly={readonly}
value={stringValue || ''}
value={stringValue}
options={{
mode: state.currentContentType,
theme: 'material',
lineNumbers: true,
indentWithTabs: true,
smartIndent: true,
lineWrapping: true,
lineWrapping: false,
}}
onBeforeChange={(editor, data, value) => handleChange(value)}
/>
@@ -175,7 +175,6 @@ export const TextValuePresentation: TabContainerPanelComponent<IDataValuePanelPr
name="value"
rows={3}
value={stringValue}
disabled={stringValue === undefined}
readOnly={readonly}
embedded
onChange={handleChange}