Merge pull request #560 from dbeaver/fix/profile

fix(plugin-user-profile): CB-1399 move change password form to user p…
This commit is contained in:
Alexey
2021-12-02 00:43:11 +03:00
committed by GitHub
36 changed files with 531 additions and 367 deletions
@@ -10,9 +10,10 @@ import { observer } from 'mobx-react-lite';
import { useLayoutEffect, useRef } from 'react';
import styled, { css } from 'reshadow';
import { TabsState, TabList, verticalTabStyles } from '@cloudbeaver/core-blocks';
import { TabsState, TabList, verticalTabStyles, SlideBox, SlideElement, ErrorBoundary, SlideOverlay, slideBoxStyles } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { composes, useStyles } from '@cloudbeaver/core-theming';
import { OptionsPanelService } from '@cloudbeaver/core-ui';
import { AdministrationItemService, filterOnlyActive } from '../AdministrationItem/AdministrationItemService';
import type { IAdministrationItemRoute } from '../AdministrationItem/IAdministrationItemRoute';
@@ -53,12 +54,21 @@ const administrationStyles = composes(
padding-top: 16px;
border-right: 2px solid;
}
content {
content-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: auto;
}
SlideBox {
flex: 1;
}
content {
height: 100%;
display: flex;
flex-direction: column;
overflow: auto;
}
`
);
@@ -73,6 +83,9 @@ export const Administration = observer<Props>(function Administration({
}) {
const contentRef = useRef<HTMLDivElement>(null);
const administrationItemService = useService(AdministrationItemService);
const optionsPanelService = useService(OptionsPanelService);
const OptionsPanel = optionsPanelService.getPanelComponent();
const items = administrationItemService.getActiveItems(configurationWizard);
const hasOnlyActive = items.some(filterOnlyActive(configurationWizard));
@@ -80,7 +93,7 @@ export const Administration = observer<Props>(function Administration({
contentRef.current?.scrollTo({ top: 0, left: 0 });
}, [activeScreen?.item]);
return styled(useStyles(verticalTabStyles, administrationStyles, tabsStyles))(
return styled(useStyles(verticalTabStyles, administrationStyles, tabsStyles, slideBoxStyles))(
<container>
<TabsState currentTabId={activeScreen?.item} orientation='vertical'>
<TabList aria-label="Administration items">
@@ -95,13 +108,26 @@ export const Administration = observer<Props>(function Administration({
/>
))}
</TabList>
<content ref={contentRef} as='div'>
<content-container ref={contentRef} as='div'>
{children}
<ItemContent
activeScreen={activeScreen}
configurationWizard={configurationWizard}
/>
</content>
<SlideBox open={optionsPanelService.active}>
<SlideElement>
<ErrorBoundary remount>
<content>
<OptionsPanel />
</content>
</ErrorBoundary>
</SlideElement>
<SlideElement>
<ErrorBoundary remount>
<content>
<ItemContent activeScreen={activeScreen} configurationWizard={configurationWizard} />
</content>
</ErrorBoundary>
<SlideOverlay onClick={() => optionsPanelService.close()} />
</SlideElement>
</SlideBox>
</content-container>
</TabsState>
</container>
);
@@ -47,9 +47,6 @@ const styles = composes(
SlideBox {
flex: 1;
}
SlideElement {
display: flex;
}
`
);
@@ -22,6 +22,7 @@ export const slideBoxStyles = composes(
height: 100%;
display: inline-block;
vertical-align:top;
white-space: normal;
transition: transform cubic-bezier(0.4, 0.0, 0.2, 1) 0.6s;
transform: translateX(calc(-100% + 120px));
@@ -58,6 +58,7 @@ export const defaultENLocale = [
['ui_file', 'File'],
['ui_changes_will_be_lost', 'Your changes will be lost'],
['ui_continue', 'Continue'],
['ui_authentication', 'Authentication'],
['root_permission_denied', "You don't have permissions"],
['root_permission_no_permission', "You don't have permission for this action"],
@@ -56,6 +56,7 @@ export const defaultRULocale = [
['ui_file', 'Файл'],
['ui_changes_will_be_lost', 'Ваши изменения будут потеряны'],
['ui_continue', 'Продолжить'],
['ui_authentication', 'Аутентификация'],
['root_permission_denied', 'Отказано в доступе'],
['root_permission_no_permission', 'У вас нет разрешения на это действие'],
@@ -6,24 +6,20 @@
* you may not use this file except in compliance with the License.
*/
import { AdministrationTopAppBarService } from '@cloudbeaver/core-administration';
import { injectable, Bootstrap } from '@cloudbeaver/core-di';
import { ServerConfigurationService } from '@cloudbeaver/plugin-administration';
import { UserInfo } from '@cloudbeaver/plugin-authentication';
import { AuthenticationProviders } from './Administration/ServerConfiguration/AuthenticationProviders';
@injectable()
export class PluginBootstrap extends Bootstrap {
constructor(
private administrationTopAppBarService: AdministrationTopAppBarService,
private readonly serverConfigurationService: ServerConfigurationService,
) {
super();
}
register(): void {
this.administrationTopAppBarService.placeholder.add(UserInfo, 4);
this.serverConfigurationService.configurationContainer.add(AuthenticationProviders, 0);
}
@@ -6,50 +6,27 @@
* you may not use this file except in compliance with the License.
*/
import { SettingsMenuService, TopNavService } from '@cloudbeaver/core-app';
import { AuthInfoService, AUTH_PROVIDER_LOCAL_ID } from '@cloudbeaver/core-authentication';
import { SettingsMenuService } from '@cloudbeaver/core-app';
import { AuthInfoService } from '@cloudbeaver/core-authentication';
import { injectable, Bootstrap } from '@cloudbeaver/core-di';
import { CommonDialogService } from '@cloudbeaver/core-dialogs';
import { ServerConfigResource } from '@cloudbeaver/core-root';
import { DATA_CONTEXT_MENU, DATA_CONTEXT_MENU_NESTED, MenuBaseItem, MenuService } from '@cloudbeaver/core-view';
import { AuthenticationService } from './AuthenticationService';
import { AuthDialogService } from './Dialog/AuthDialogService';
import { UserInfo } from './UserInfo';
import { ChangeUserPasswordDialog } from './UserMenu/ChangeUserPasswordDialog/ChangeUserPasswordDialog';
import { MENU_USER_PROFILE } from './UserMenu/MENU_USER_PROFILE';
@injectable()
export class PluginBootstrap extends Bootstrap {
constructor(
private serverConfigResource: ServerConfigResource,
private authDialogService: AuthDialogService,
private authenticationService: AuthenticationService,
private authInfoService: AuthInfoService,
private settingsMenuService: SettingsMenuService,
private topNavService: TopNavService,
private menuService: MenuService,
private readonly commonDialogService: CommonDialogService,
private readonly serverConfigResource: ServerConfigResource,
private readonly authDialogService: AuthDialogService,
private readonly authenticationService: AuthenticationService,
private readonly authInfoService: AuthInfoService,
private readonly settingsMenuService: SettingsMenuService,
) {
super();
}
register(): void {
this.menuService.addCreator({
isApplicable:
context => {
const profile = context.get(DATA_CONTEXT_MENU) === MENU_USER_PROFILE
&& !context.get(DATA_CONTEXT_MENU_NESTED);
return profile && !!this.authInfoService.userInfo?.linkedAuthProviders.includes(AUTH_PROVIDER_LOCAL_ID);
},
getItems: (context, items) => [
...items,
new MenuBaseItem('change-password', 'authentication_user_password_change_menu_title', undefined, {
onSelect: () => this.commonDialogService.open(ChangeUserPasswordDialog, null),
}),
],
});
this.settingsMenuService.addMenuItem(
SettingsMenuService.settingsMenuToken,
{
@@ -71,7 +48,6 @@ export class PluginBootstrap extends Bootstrap {
onClick: this.authenticationService.logout.bind(this.authenticationService),
}
);
this.topNavService.placeholder.add(UserInfo, 4);
}
load(): void | Promise<void> { }
@@ -1,60 +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 styled, { css } from 'reshadow';
import { topMenuStyles } from '@cloudbeaver/core-app';
import { AuthInfoService, DATA_CONTEXT_USER } from '@cloudbeaver/core-authentication';
import { IconOrImage } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { useStyles } from '@cloudbeaver/core-theming';
import { ContextMenu } from '@cloudbeaver/core-ui';
import { useMenu } from '@cloudbeaver/core-view';
import { MENU_USER_PROFILE } from './UserMenu/MENU_USER_PROFILE';
import { userMenuStyles } from './UserMenu/userMenuStyles';
const styles = css`
user {
height: 100%;
display: flex;
align-items: center;
}
IconOrImage {
display: block;
width: 24px;
}
user-name {
display: block;
line-height: initial;
margin-left: 8px;
}
`;
export const UserInfo = observer(function UserInfo() {
const authInfoService = useService(AuthInfoService);
const style = useStyles(styles, userMenuStyles);
const menu = useMenu(MENU_USER_PROFILE);
menu.context.set(DATA_CONTEXT_USER, authInfoService.userInfo);
if (!authInfoService.userInfo) {
return null;
}
return styled(style)(
<ContextMenu menu={menu} style={[topMenuStyles]} rtl modal>
<user>
<user-icon>
<IconOrImage icon='user' viewBox='0 0 28 28' />
</user-icon>
<user-name>{authInfoService.userInfo.displayName || authInfoService.userInfo.userId}</user-name>
</user>
</ContextMenu>
);
});
@@ -1,157 +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 { observable } from 'mobx';
import { observer } from 'mobx-react-lite';
import { useState, useCallback, useEffect } from 'react';
import styled, { css } from 'reshadow';
import { UsersResource } from '@cloudbeaver/core-authentication';
import { BASE_CONTAINERS_STYLES, Container, ErrorMessage, Group, InputField, SubmittingForm, useErrorDetails, useFocus } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { CommonDialogWrapper, DialogComponent, DialogComponentProps } from '@cloudbeaver/core-dialogs';
import { NotificationService } from '@cloudbeaver/core-events';
import { useTranslate } from '@cloudbeaver/core-localization';
import { GQLErrorCatcher } from '@cloudbeaver/core-sdk';
import { composes, useStyles } from '@cloudbeaver/core-theming';
import { ChangeUserPasswordDialogFooter } from './ChangeUserPasswordDialogFooter';
const styles = composes(
css`
ErrorMessage {
composes: theme-background-secondary theme-text-on-secondary from global;
}
`,
css`
ErrorMessage {
flex: 1;
}
`);
interface IState {
oldPassword: string;
newPassword: string;
repeatedPassword: string;
error: GQLErrorCatcher;
customError: string;
submitting: boolean;
}
export const ChangeUserPasswordDialog: DialogComponent<null, null> = observer(
function ChangeUserPasswordDialog({
payload,
resolveDialog,
rejectDialog,
}: DialogComponentProps<null, null>) {
const [focusedRef] = useFocus<HTMLInputElement>({});
const usersResource = useService(UsersResource);
const notificationService = useService(NotificationService);
const style = useStyles(styles, BASE_CONTAINERS_STYLES);
const translate = useTranslate();
const [state] = useState<IState>(() => observable({
oldPassword: '', newPassword: '', repeatedPassword: '', error: new GQLErrorCatcher(), customError: '', submitting: false,
}));
const { details: errorDetails, open: openErrorDetails } = useErrorDetails(state.error.exception);
const formFilled
= state.newPassword.length > 0 && state.repeatedPassword.length > 0 && state.oldPassword.length > 0;
const disabled = state.submitting;
const change = useCallback(async () => {
state.error.clear();
if (state.newPassword !== state.repeatedPassword) {
state.customError = 'authentication_user_passwords_not_match';
return;
}
try {
state.submitting = true;
await usersResource.updateLocalPassword(state.oldPassword, state.newPassword);
notificationService.logSuccess({ title: 'authentication_user_password_change_success' });
resolveDialog();
} catch (exeption) {
if (!state.error.catch(exeption)) {
state.customError = exeption.message;
}
} finally {
state.submitting = false;
}
}, [state, resolveDialog, notificationService, usersResource]);
useEffect(() => {
focusedRef.current?.focus();
}, []);
return styled(style)(
<CommonDialogWrapper
size='medium'
title="authentication_user_password_change_dialog_title"
footer={(
<ChangeUserPasswordDialogFooter
submitting={state.submitting}
formFilled={formFilled}
onCancel={rejectDialog}
onChange={change}
>
{(state.error.responseMessage || state.customError) && (
<ErrorMessage
text={state.error.responseMessage || translate(state.customError)}
hasDetails={errorDetails?.hasDetails}
onShowDetails={openErrorDetails}
/>
)}
</ChangeUserPasswordDialogFooter>
)}
onReject={rejectDialog}
>
<SubmittingForm onSubmit={change}>
<Container>
<Group small gap center>
<InputField
ref={focusedRef}
type='password'
name='oldPassword'
state={state}
disabled={disabled}
mapValue={(value => value.trim())}
small
required
>
{translate('authentication_user_current_password')}
</InputField>
<InputField
type='password'
name='newPassword'
autoComplete='new-password'
state={state}
disabled={disabled}
mapValue={(value => value.trim())}
small
required
>
{translate('authentication_user_new_password')}
</InputField>
<InputField
type='password'
name='repeatedPassword'
state={state}
disabled={disabled}
mapValue={(value => value.trim())}
small
required
>
{translate('authentication_user_password_repeat')}
</InputField>
</Group>
</Container>
</SubmittingForm>
</CommonDialogWrapper>
);
}
);
@@ -1,60 +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 styled, { css } from 'reshadow';
import { Button } from '@cloudbeaver/core-blocks';
import { useTranslate } from '@cloudbeaver/core-localization';
const styles = css`
footer-container {
display: flex;
width: min-content;
flex: 1;
align-items: center;
justify-content: flex-end;
}
footer-container > :not(:first-child) {
margin-left: 16px;
}
Button {
flex: 0 0 auto;
}
`;
export interface Props {
onChange: () => void;
onCancel: () => void;
formFilled: boolean;
submitting: boolean;
className?: string;
}
export const ChangeUserPasswordDialogFooter = observer<Props>(function ChangeUserPasswordDialogFooter({
onChange,
onCancel,
formFilled,
submitting,
className,
children,
}) {
const translate = useTranslate();
return styled(styles)(
<footer-container className={className}>
{children}
<Button disabled={submitting} type="button" mod={['outlined']} onClick={onCancel}>
{translate('ui_processing_cancel')}
</Button>
<Button disabled={submitting || !formFilled} loading={submitting} type="button" mod={['unelevated']} onClick={onChange}>
{translate('authentication_user_password_change_dialog_submit')}
</Button>
</footer-container>
);
});
@@ -4,5 +4,3 @@ export default manifest;
export * from './AuthenticationService';
export * from './Dialog/AuthDialogService';
export * from './UserMenu/MENU_USER_PROFILE';
export * from './UserInfo';
@@ -14,8 +14,6 @@ export default [
['authentication_user_name', 'User name'],
['authentication_user_credentials', 'Credentials'],
['authentication_user_meta_parameters', 'Parameters'],
['authentication_user_current_password', 'Current password'],
['authentication_user_new_password', 'New password'],
['authentication_user_password', 'User password'],
['authentication_user_password_repeat', 'Repeat password'],
['authentication_user_role', 'User role'],
@@ -25,10 +23,6 @@ export default [
['authentication_user_passwords_not_match', "Passwords don't match"],
['authentication_user_login_already_exists', 'The user with this name already exists'],
['authentication_user_login_cant_be_used', 'Sorry, that name cannot be used'],
['authentication_user_password_change_dialog_title', 'Change password (local)'],
['authentication_user_password_change_menu_title', 'Change local password'],
['authentication_user_password_change_success', 'Password was changed successfully'],
['authentication_user_password_change_dialog_submit', 'Change'],
['authentication_user_icon_tooltip', 'User'],
['authentication_role_icon_tooltip', 'Role'],
];
@@ -8,8 +8,6 @@ export default [
['authentication_user_name', 'Nome utente'],
['authentication_user_credentials', 'Credenziali'],
['authentication_user_meta_parameters', 'Parameters'],
['authentication_user_current_password', 'Password corrente'],
['authentication_user_new_password', 'Nuova password'],
['authentication_user_password', 'Password utente'],
['authentication_user_password_repeat', 'Ripeti password'],
['authentication_user_role', 'Ruolo utente'],
@@ -19,10 +17,6 @@ export default [
['authentication_user_passwords_not_match', 'Le Passwords non coincidono'],
['authentication_user_login_already_exists', 'Esiste già un utente con quel nome'],
['authentication_user_login_cant_be_used', 'Quel nome non può essere usato'],
['authentication_user_password_change_dialog_title', 'Modifica la password (locale)'],
['authentication_user_password_change_menu_title', 'Modifica la password locale'],
['authentication_user_password_change_success', 'Password modificata con successo'],
['authentication_user_password_change_dialog_submit', 'Modifica'],
['authentication_user_icon_tooltip', 'Utente'],
['authentication_role_icon_tooltip', 'Ruolo'],
];
@@ -14,8 +14,6 @@ export default [
['authentication_user_name', 'Имя'],
['authentication_user_credentials', 'Учетные данные'],
['authentication_user_meta_parameters', 'Параметры'],
['authentication_user_current_password', 'Текущий пароль'],
['authentication_user_new_password', 'Новый пароль'],
['authentication_user_password', 'Пароль'],
['authentication_user_password_repeat', 'Повторите пароль'],
['authentication_user_role', 'Роль'],
@@ -25,10 +23,6 @@ export default [
['authentication_user_passwords_not_match', 'Пароли не совпадают'],
['authentication_user_login_already_exists', 'Пользователь с таким именем уже существует'],
['authentication_user_login_cant_be_used', 'Данное имя нельзя использовать'],
['authentication_user_password_change_dialog_title', 'Изменение пароля (локального)'],
['authentication_user_password_change_menu_title', 'Изменить локальный пароль'],
['authentication_user_password_change_success', 'Пароль успешно изменен'],
['authentication_user_password_change_dialog_submit', 'Изменить'],
['authentication_user_icon_tooltip', 'Пользователь'],
['authentication_role_icon_tooltip', 'Роль'],
];
@@ -0,0 +1,17 @@
# dependencies
/node_modules
# testing
/coverage
# production
/lib
# misc
.DS_Store
.env*
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
@@ -0,0 +1,46 @@
{
"name": "@cloudbeaver/plugin-user-profile-administration",
"sideEffects": false,
"version": "0.1.0",
"description": "",
"license": "Apache-2.0",
"main": "lib/index.js",
"module": "lib/index.js",
"types": "src/index.ts",
"scripts": {
"prebuild": "rimraf lib/*",
"build": "webpack --config ../../configs/webpack.plugin.config.js",
"postbuild": "tsc",
"lint": "eslint ./src/ --ext .ts,.tsx",
"lint-fix": "eslint ./src/ --ext .ts,.tsx --fix"
},
"peerDependencies": {
"@cloudbeaver/core-administration": "^0.1.0",
"@cloudbeaver/core-app": "^0.1.0",
"@cloudbeaver/core-authentication": "^0.1.0",
"@cloudbeaver/core-blocks": "^0.1.0",
"@cloudbeaver/core-connections": "^0.1.0",
"@cloudbeaver/core-di": "^0.1.0",
"@cloudbeaver/core-dialogs": "^0.1.0",
"@cloudbeaver/core-events": "^0.1.0",
"@cloudbeaver/core-executor": "^0.1.0",
"@cloudbeaver/core-extensions": "^0.1.0",
"@cloudbeaver/core-localization": "^0.1.0",
"@cloudbeaver/core-notifications": "^0.1.0",
"@cloudbeaver/core-plugin": "^0.1.0",
"@cloudbeaver/core-product": "^0.1.0",
"@cloudbeaver/core-root": "^0.1.0",
"@cloudbeaver/core-routing": "^0.1.0",
"@cloudbeaver/core-sdk": "^0.1.0",
"@cloudbeaver/core-settings": "^0.1.0",
"@cloudbeaver/core-theming": "^0.1.0",
"@cloudbeaver/core-ui": "^0.1.0",
"@cloudbeaver/core-utils": "^0.1.0",
"@cloudbeaver/core-view": "^0.1.0",
"@cloudbeaver/plugin-user-profile": "^0.1.0",
"react": "^16.x.x",
"mobx": "^6.x.x",
"mobx-react-lite": "^3.x.x",
"reshadow": "^0.x.x"
}
}
@@ -0,0 +1,26 @@
/*
* 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 { AdministrationTopAppBarService } from '@cloudbeaver/core-administration';
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { UserMenu } from '@cloudbeaver/plugin-user-profile';
@injectable()
export class PluginBootstrap extends Bootstrap {
constructor(
private readonly administrationTopAppBarService: AdministrationTopAppBarService
) {
super();
}
register(): void {
this.administrationTopAppBarService.placeholder.add(UserMenu, 4);
}
load(): void { }
}
@@ -0,0 +1,3 @@
import { userProfileAdministrationPlugin } from './manifest';
export default userProfileAdministrationPlugin;
@@ -0,0 +1,21 @@
/*
* 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 { PluginManifest } from '@cloudbeaver/core-di';
import { PluginBootstrap } from './PluginBootstrap';
export const userProfileAdministrationPlugin: PluginManifest = {
info: {
name: 'User profile administration plugin',
},
providers: [
PluginBootstrap,
],
};
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "lib"
},
"include": ["src"],
"exclude": ["**/node_modules", "**/.*/"],
}
@@ -6,34 +6,22 @@
* you may not use this file except in compliance with the License.
*/
import { TopNavService } from '@cloudbeaver/core-app';
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { DATA_CONTEXT_MENU, DATA_CONTEXT_MENU_NESTED, MenuBaseItem, MenuService } from '@cloudbeaver/core-view';
import { MENU_USER_PROFILE } from '@cloudbeaver/plugin-authentication';
import { UserProfileService } from './UserProfileService';
import { UserMenu } from './UserMenu/UserMenu';
@injectable()
export class PluginBootstrap extends Bootstrap {
constructor(
private menuService: MenuService,
private userProfileService: UserProfileService
private readonly topNavService: TopNavService
) {
super();
}
register(): void {
this.menuService.addCreator({
isApplicable:
context => context.get(DATA_CONTEXT_MENU) === MENU_USER_PROFILE
&& !context.get(DATA_CONTEXT_MENU_NESTED),
getItems: (context, items) => [
new MenuBaseItem('profile', 'plugin_user_profile_menu', undefined, {
onSelect: () => this.userProfileService.open(),
}),
...items,
],
});
this.topNavService.placeholder.add(UserMenu, 4);
}
load(): void {}
load(): void { }
}
@@ -0,0 +1,62 @@
/*
* 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 { IconOrImage } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { useTranslate } from '@cloudbeaver/core-localization';
import type { UserInfo as IUserInfo } from '@cloudbeaver/core-sdk';
import { composes, useStyles } from '@cloudbeaver/core-theming';
import { UserProfileService } from './UserProfileService';
const styles = composes(
css`
user {
composes: theme-ripple from global;
}
`,
css`
user {
height: 100%;
display: flex;
align-items: center;
padding: 0 16px;
cursor: pointer;
}
IconOrImage {
display: block;
width: 24px;
}
user-name {
display: block;
line-height: initial;
margin-left: 8px;
}
`);
interface Props {
info: IUserInfo;
}
export const UserInfo = observer<Props>(function UserInfo({ info }) {
const translate = useTranslate();
const style = useStyles(styles);
const userProfileService = useService(UserProfileService);
return styled(style)(
<user title={translate('plugin_user_profile_menu')} onClick={() => userProfileService.open()}>
<user-icon>
<IconOrImage icon='user' viewBox='0 0 28 28' />
</user-icon>
<user-name>{info.displayName || info.userId}</user-name>
</user>
);
});
@@ -0,0 +1,43 @@
/*
* 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 from 'reshadow';
import { topMenuStyles } from '@cloudbeaver/core-app';
import { AuthInfoService, DATA_CONTEXT_USER } from '@cloudbeaver/core-authentication';
import { Icon } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { useStyles } from '@cloudbeaver/core-theming';
import { ContextMenu } from '@cloudbeaver/core-ui';
import { useMenu } from '@cloudbeaver/core-view';
import { UserInfo } from '../UserInfo';
import { MENU_USER_PROFILE } from './MENU_USER_PROFILE';
import { userMenuStyles } from './userMenuStyles';
export const UserMenu = observer(function UserMenu() {
const style = useStyles(userMenuStyles);
const authInfoService = useService(AuthInfoService);
const menu = useMenu(MENU_USER_PROFILE);
menu.context.set(DATA_CONTEXT_USER, authInfoService.userInfo);
if (!authInfoService.userInfo) {
return null;
}
return styled(style)(
<>
<UserInfo info={authInfoService.userInfo} />
<ContextMenu menu={menu} style={[topMenuStyles]} rtl modal>
<Icon name="angle" viewBox="0 0 15 8" />
</ContextMenu>
</>
);
});
@@ -11,11 +11,17 @@ import { css } from 'reshadow';
export const userMenuStyles = css`
ContextMenu {
opacity: 1;
padding: 0 16px !important;
padding: 0 !important;
height: 48px;
cursor: pointer;
background: none;
border: none;
outline: none !important;
& Icon {
background: #47a0dd;
width: 16px;
height: 100%;
padding: 0 16px;
}
}
`;
@@ -0,0 +1,34 @@
/*
* 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 from 'reshadow';
import { BASE_CONTAINERS_STYLES, ColoredContainer, Container, TabPanel } from '@cloudbeaver/core-blocks';
import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming';
import { ChangePassword } from './ChangePassword/ChangePassword';
interface Props {
className?: string;
style?: ComponentStyle;
}
export const AuthenticationPanel = observer<Props>(function AuthenticationPanel({ className, style }) {
const styles = useStyles(style, BASE_CONTAINERS_STYLES);
return styled(styles)(
<TabPanel tabId='authentication' className={className}>
<ColoredContainer wrap overflow parent gap>
<Container medium gap>
<ChangePassword />
</Container>
</ColoredContainer>
</TabPanel>
);
});
@@ -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 styled from 'reshadow';
import { Tab } from '@cloudbeaver/core-blocks';
import { useTranslate } from '@cloudbeaver/core-localization';
import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming';
interface Props {
className?: string;
style?: ComponentStyle;
}
export const AuthenticationTab = observer<Props>(function AuthenticationTab({ className, style }) {
const translate = useTranslate();
const styles = useStyles(style);
return styled(styles)(
<Tab tabId='authentication' style={style} className={className}>{translate('ui_authentication')}</Tab>
);
});
@@ -0,0 +1,74 @@
/*
* 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 from 'reshadow';
import { BASE_CONTAINERS_STYLES, Button, Group, GroupItem, GroupTitle, InputField } from '@cloudbeaver/core-blocks';
import { useTranslate } from '@cloudbeaver/core-localization';
import { useStyles } from '@cloudbeaver/core-theming';
import { useChangePassword } from './useChangePassword';
export const ChangePassword = observer(function ChangePassword() {
const translate = useTranslate();
const state = useChangePassword();
const disabled = state.submitting;
return styled(useStyles(BASE_CONTAINERS_STYLES))(
<Group form gap>
<GroupTitle>{translate('plugin_user_profile_authentication_change_password')}</GroupTitle>
<InputField
type='password'
name='oldPassword'
state={state.config}
disabled={disabled}
mapValue={(value => value.trim())}
small
required
>
{translate('plugin_user_profile_authentication_change_password_current_password')}
</InputField>
<InputField
type='password'
name='password'
autoComplete='new-password'
state={state.config}
disabled={disabled}
mapValue={(value => value.trim())}
small
required
>
{translate('plugin_user_profile_authentication_change_password_new_password')}
</InputField>
<InputField
type='password'
name='repeatedPassword'
state={state.config}
disabled={disabled}
mapValue={(value => value.trim())}
small
required
>
{translate('plugin_user_profile_authentication_change_password_repeat_password')}
</InputField>
<GroupItem>
<Button
disabled={disabled || !state.formFilled}
loading={state.submitting}
type="button"
mod={['unelevated']}
onClick={state.changePassword}
>
{translate('plugin_user_profile_authentication_change_password_submit_label')}
</Button>
</GroupItem>
</Group>
);
});
@@ -0,0 +1,76 @@
/*
* 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 { action, computed, observable } from 'mobx';
import { UsersResource } from '@cloudbeaver/core-authentication';
import { useObservableRef } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
interface IConfig {
oldPassword: string;
password: string;
repeatedPassword: string;
}
interface IState {
config: IConfig;
submitting: boolean;
readonly formFilled: boolean;
changePassword: () => Promise<void>;
resetConfig: () => void;
}
export function useChangePassword(): IState {
const usersResource = useService(UsersResource);
const notificationService = useService(NotificationService);
return useObservableRef(() => ({
config: {
oldPassword: '',
password: '',
repeatedPassword: '',
},
submitting: false,
get formFilled() {
return this.config.password.length > 0 && this.config.oldPassword.length > 0
&& this.config.repeatedPassword.length > 0;
},
async changePassword() {
if (this.config.password !== this.config.repeatedPassword) {
this.notificationService.logError({ title: 'plugin_user_profile_authentication_change_password_passwords_not_match' });
return;
}
try {
this.submitting = true;
await usersResource.updateLocalPassword(this.config.oldPassword, this.config.password);
notificationService.logSuccess({ title: 'plugin_user_profile_authentication_change_password_success' });
this.resetConfig();
} catch (exception) {
notificationService.logException(exception);
} finally {
this.submitting = false;
}
},
resetConfig() {
this.config.oldPassword = '';
this.config.password = '';
this.config.repeatedPassword = '';
},
}),
{
config: observable,
submitting: observable.ref,
formFilled: computed,
changePassword: action.bound,
resetConfig: action,
},
{ usersResource, notificationService });
}
@@ -9,11 +9,14 @@
import { observer } from 'mobx-react-lite';
import styled, { css } from 'reshadow';
import { AUTH_PROVIDER_LOCAL_ID } from '@cloudbeaver/core-authentication';
import { BASE_CONTAINERS_STYLES, Button, IconOrImage, TabList, TabsState, UNDERLINE_TAB_STYLES } from '@cloudbeaver/core-blocks';
import { useTranslate } from '@cloudbeaver/core-localization';
import type { UserInfo } from '@cloudbeaver/core-sdk';
import { composes, useStyles } from '@cloudbeaver/core-theming';
import { AuthenticationPanel } from './Authentication/AuthenticationPanel';
import { AuthenticationTab } from './Authentication/AuthenticationTab';
import type { IUserProfileFormState } from './IUserProfileFormState';
import { UserInfoPanel } from './UserInfo/UserInfoPanel';
import { UserInfoTab } from './UserInfo/UserInfoTab';
@@ -126,6 +129,8 @@ export const UserForm = observer<Props>(function UserForm({
const style = [tabsStyles, UNDERLINE_TAB_STYLES];
const styles = useStyles(style, BASE_CONTAINERS_STYLES, topBarStyles, formStyles);
const localProvider = user.linkedAuthProviders.includes(AUTH_PROVIDER_LOCAL_ID);
return styled(styles)(
<flex-box>
<TabsState>
@@ -141,6 +146,7 @@ export const UserForm = observer<Props>(function UserForm({
</status-message>
<TabList style={style} disabled={state.info.disabled}>
<UserInfoTab style={style} />
{localProvider && <AuthenticationTab style={style} />}
{/* <UserAuthProvidersTab style={style} /> */}
</TabList>
</top-bar-tabs>
@@ -158,6 +164,7 @@ export const UserForm = observer<Props>(function UserForm({
</top-bar>
<content-box>
<UserInfoPanel user={user} style={style} />
{localProvider && <AuthenticationPanel style={style} />}
{/* <UserAuthProviderPanel user={user} style={style} /> */}
</content-box>
</TabsState>
@@ -45,7 +45,6 @@ export const UserInfoPanel = observer<Props>(function UserInfoPanel({
minLength={1}
state={user}
mod='surface'
disabled
readOnly
required
tiny
@@ -59,7 +58,6 @@ export const UserInfoPanel = observer<Props>(function UserInfoPanel({
minLength={1}
state={user}
mod='surface'
disabled
readOnly
required
tiny
@@ -68,14 +66,12 @@ export const UserInfoPanel = observer<Props>(function UserInfoPanel({
{translate('plugin_user_profile_info_displayName')}
</InputField>
</Container>
<Loader state={userMetaParameters} inline>
{() => userMetaParameters.data.length > 0 && styled(styles)(
<Container wrap gap>
<ObjectPropertyInfoForm
state={user.metaParameters}
properties={userMetaParameters.data}
disabled
readOnly
tiny
fill
@@ -89,7 +85,6 @@ export const UserInfoPanel = observer<Props>(function UserInfoPanel({
<AuthTokenList user={user} />
</Group>
</Container>
</ColoredContainer>
</TabPanel>
);
@@ -1,3 +1,6 @@
import { userProfilePlugin } from './manifest';
export default userProfilePlugin;
export * from './UserMenu/MENU_USER_PROFILE';
export * from './UserMenu/UserMenu';
@@ -7,4 +7,12 @@ export default [
['plugin_user_profile_info', 'User Info'],
['plugin_user_profile_auth_tokens', 'Auth Tokens'],
['plugin_user_profile_auth_providers_active', 'Authentication is active'],
['plugin_user_profile_authentication_change_password', 'Change password'],
['plugin_user_profile_authentication_change_password_current_password', 'Current password'],
['plugin_user_profile_authentication_change_password_new_password', 'New password'],
['plugin_user_profile_authentication_change_password_repeat_password', 'Repeat password'],
['plugin_user_profile_authentication_change_password_success', 'Password was changed successfully'],
['plugin_user_profile_authentication_change_password_submit_label', 'Change'],
['plugin_user_profile_authentication_change_password_passwords_not_match', "Passwords don't match"],
];
@@ -7,4 +7,12 @@ export default [
['plugin_user_profile_info', 'User Info'],
['plugin_user_profile_auth_tokens', 'Auth Tokens'],
['plugin_user_profile_auth_providers_active', 'Authentication is active'],
['plugin_user_profile_authentication_change_password', 'Modifica la password'],
['plugin_user_profile_authentication_change_password_current_password', 'Password corrente'],
['plugin_user_profile_authentication_change_password_new_password', 'Nuova password'],
['plugin_user_profile_authentication_change_password_repeat_password', 'Ripeti password'],
['plugin_user_profile_authentication_change_password_success', 'Password modificata con successo'],
['plugin_user_profile_authentication_change_password_submit_label', 'Modifica'],
['plugin_user_profile_authentication_change_password_passwords_not_match', 'Le Passwords non coincidono'],
];
@@ -7,4 +7,12 @@ export default [
['plugin_user_profile_info', 'Информация'],
['plugin_user_profile_auth_tokens', 'Токены авторизации'],
['plugin_user_profile_auth_providers_active', 'Авторизация выполнена'],
['plugin_user_profile_authentication_change_password', 'Смена пароля'],
['plugin_user_profile_authentication_change_password_current_password', 'Текущий пароль'],
['plugin_user_profile_authentication_change_password_new_password', 'Новый пароль'],
['plugin_user_profile_authentication_change_password_repeat_password', 'Повторите пароль'],
['plugin_user_profile_authentication_change_password_success', 'Пароль был успешно изменен'],
['plugin_user_profile_authentication_change_password_submit_label', 'Сменить'],
['plugin_user_profile_authentication_change_password_passwords_not_match', 'Пароли не совпадают'],
];
@@ -20,6 +20,7 @@ import sqlEditor from '@cloudbeaver/plugin-sql-editor';
import sqlEditorTab from '@cloudbeaver/plugin-sql-editor-navigation-tab';
import sqlEditorScreen from '@cloudbeaver/plugin-sql-editor-screen';
import userProfile from '@cloudbeaver/plugin-user-profile';
import userProfileAdministration from '@cloudbeaver/plugin-user-profile-administration';
import { defaultProductManifest } from './manifest';
@@ -39,6 +40,7 @@ const PLUGINS: PluginManifest[] = [
sqlEditorTab,
sqlEditorScreen,
userProfile,
userProfileAdministration,
defaultProductManifest,
connectionPlugin,
connectionAdministration,