Merge pull request #42 from dbeaver/feat-authentication

refactor(authentication): reuse Tab block
This commit is contained in:
Alexey
2020-05-17 15:52:59 +03:00
committed by GitHub
11 changed files with 245 additions and 209 deletions
@@ -7,12 +7,11 @@
*/
import { observer } from 'mobx-react';
import {
useTabState, Tab as BaseTab, TabList
} from 'reakit/Tab';
import styled, { css } from 'reshadow';
import { SubmittingForm, ErrorMessage } from '@dbeaver/core/blocks';
import {
SubmittingForm, ErrorMessage, TabsState, TabList, Tab, TabTitle
} from '@dbeaver/core/blocks';
import { useController } from '@dbeaver/core/di';
import { DialogComponent, CommonDialogWrapper } from '@dbeaver/core/dialogs';
import { useTranslate } from '@dbeaver/core/localization';
@@ -24,7 +23,7 @@ import { AuthProviderForm } from './AuthProviderForm/AuthProviderForm';
const styles = composes(
css`
BaseTab {
Tab {
composes: theme-ripple theme-background-secondary theme-text-on-secondary from global;
}
ErrorMessage {
@@ -53,34 +52,19 @@ const styles = composes(
flex-direction: column;
}
BaseTab {
outline: none;
}
TabList {
box-sizing: border-box;
display: inline-flex;
width: 100%;
padding-left: 24px;
}
BaseTab {
Tab {
composes: theme-typography--body2 from global;
text-transform: uppercase;
padding: 12px 16px;
border-top: solid 2px transparent;
height: 48px;
font-weight: normal;
&:global([aria-selected='true']) {
border-top-color: #fd1d48;
&:before {
display: none;
}
}
&:not(:global([aria-selected='true'])) {
cursor: pointer;
background-color: transparent !important;
&:global([aria-selected=true]) {
font-weight: normal !important;
}
}
AuthProviderForm {
@@ -99,56 +83,52 @@ export const AuthDialog: DialogComponent<null, null> = observer(
function AuthDialog(props) {
const controller = useController(AuthDialogController, props.rejectDialog);
const translate = useTranslate();
const tab = useTabState({
selectedId: controller.provider?.id,
});
tab.selectedId = controller.provider?.id || null;
return styled(useStyles(styles))(
<CommonDialogWrapper
title={translate('authentication_login_dialog_title')}
noBodyPadding
header={(
<TabList {...tab} aria-label="My tabs">
{controller.providers.map(provider => (
<BaseTab
{...tab}
key={provider.id}
type='button'
stopId={provider.id}
onClick={() => controller.selectProvider(provider.id)}
>
{provider.label}
</BaseTab>
))}
</TabList>
)}
footer={(
<AuthDialogFooter
isAuthenticating={controller.isAuthenticating}
onLogin={controller.login}
/>
)}
onReject={props.options?.persistent ? undefined : props.rejectDialog}
>
<SubmittingForm onSubmit={controller.login}>
{controller.provider && (
<AuthProviderForm
provider={controller.provider}
credentials={controller.credentials}
authenticate={controller.isAuthenticating}
<TabsState currentTabId={controller.provider?.id || null}>
<CommonDialogWrapper
title={translate('authentication_login_dialog_title')}
noBodyPadding
header={(
<TabList>
{controller.providers.map(provider => (
<Tab
key={provider.id}
tabId={provider.id}
onOpen={() => controller.selectProvider(provider.id)}
>
<TabTitle title={provider.label} />
</Tab>
))}
</TabList>
)}
footer={(
<AuthDialogFooter
isAuthenticating={controller.isAuthenticating}
onLogin={controller.login}
/>
)}
{!controller.provider && <>Select available provider</>}
</SubmittingForm>
{controller.error.responseMessage && (
<ErrorMessage
text={controller.error.responseMessage}
hasDetails={controller.error.hasDetails}
onShowDetails={controller.showDetails}
/>
)}
</CommonDialogWrapper>
onReject={props.options?.persistent ? undefined : props.rejectDialog}
>
<SubmittingForm onSubmit={controller.login}>
{controller.provider && (
<AuthProviderForm
provider={controller.provider}
credentials={controller.credentials}
authenticate={controller.isAuthenticating}
/>
)}
{!controller.provider && <>Select available provider</>}
</SubmittingForm>
{controller.error.responseMessage && (
<ErrorMessage
text={controller.error.responseMessage}
hasDetails={controller.error.hasDetails}
onShowDetails={controller.showDetails}
/>
)}
</CommonDialogWrapper>
</TabsState>
);
}
);
+49 -41
View File
@@ -36,19 +36,15 @@
Tab {
composes: theme-typography--body2 from global;
position: relative;
display: flex;
flex-shrink: 0;
text-align: left;
align-items: center;
outline: none;
font-weight: normal;
cursor: pointer;
padding: 0;
height: 48px;
padding: 0 24px 0 12px;
border-top: solid 2px transparent;
&:global([aria-selected=true]) {
&:global([aria-selected="true"]) {
font-weight: 500;
cursor: auto;
border-top-color: $color-negative;
@@ -58,52 +54,64 @@
}
}
&:not(:global([aria-selected=true])) {
&:not(:global([aria-selected="true"])) {
background-color: transparent !important;
}
}
tab-icon {
height: 22px;
padding-right: 8px;
tab-container {
height: 100%;
display: flex;
flex-shrink: 0;
text-align: left;
align-items: center;
& StaticImage,
& img {
width: 22px;
}
& placeholder {
width: 22px;
tab-icon {
height: 22px;
padding-left: 12px;
StaticImage,
img {
width: 22px;
}
placeholder {
width: 22px;
height: 22px;
}
}
}
tab-title placeholder {
width: 80px;
height: 16px;
}
tab-title {
padding: 0 12px;
}
tab-action:before {
content: "";
display: block;
left: -2px;
top: -2px;
height: 12px;
width: 12px;
position: absolute;
cursor: pointer;
}
tab-title placeholder {
width: 80px;
height: 16px;
}
tab-action {
height: 8px;
width: 8px;
align-self: flex-start;
position: absolute;
top: 4px;
right: 4px;
& Icon {
tab-action:before {
content: "";
display: block;
left: -4px;
top: 0;
height: 16px;
width: 16px;
position: absolute;
cursor: pointer;
}
tab-action {
position: relative;
height: 8px;
width: 8px;
align-self: flex-start;
padding: 4px;
padding-left: 0;
Icon {
display: block;
}
}
}
@@ -18,7 +18,7 @@ import { TabsContext } from '../TabsContext';
type TabProps = PropsWithChildren<{
tabId: string;
className?: string;
onOpen: (tabId: string) => void;
onOpen?: (tabId: string) => void;
onClose?: (tabId: string) => void;
}>
@@ -36,8 +36,10 @@ export function Tab({
const handleOpen = useCallback((e: React.MouseEvent<any>) => {
e.preventDefault();
onOpen(tabId);
}, []);
if (onOpen) {
onOpen(tabId);
}
}, [onOpen]);
const handleClose = useCallback((e: React.MouseEvent<any>) => {
e.preventDefault();
@@ -54,12 +56,14 @@ export function Tab({
className={className}
onClick={handleOpen}
>
{children}
{onClose && (
<tab-action as="div" onClick={handleClose}>
<Icon name="cross-bold" viewBox="0 0 7 8"/>
</tab-action>
)}
<tab-container as='div'>
{children}
{onClose && (
<tab-action as="div" onClick={handleClose}>
<Icon name="cross-bold" viewBox="0 0 7 8"/>
</tab-action>
)}
</tab-container>
</BaseTab>
);
}
@@ -0,0 +1,22 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 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 { useContext } from 'react';
import { TabList as BaseTabList, TabListOptions } from 'reakit/Tab';
import { TabsContext } from './TabsContext';
export function TabList(props: React.PropsWithChildren<TabListOptions>) {
const state = useContext(TabsContext);
if (!state) {
throw new Error('Tabs context was not provided');
}
return <BaseTabList {...props} {...state} />;
}
@@ -6,15 +6,12 @@
* you may not use this file except in compliance with the License.
*/
import {
useMemo, PropsWithChildren, ReactNode, useEffect,
} from 'react';
import { useTabState } from 'reakit/Tab';
import { PropsWithChildren, ReactNode } from 'react';
import styled from 'reshadow';
import { useStyles, Style } from '@dbeaver/core/theming';
import { TabsContext } from '../TabsContext';
import { TabsState } from '../TabsState';
type TabsBoxProps = PropsWithChildren<{
currentTabId: string;
@@ -30,16 +27,9 @@ export function TabsBox({
className,
style = [],
}: TabsBoxProps) {
const state = useTabState({
selectedId: currentTabId,
manual: true,
});
state.selectedId = currentTabId;
useEffect(() => state.select(currentTabId), [currentTabId]); // hack currentId and selectedId not works
const value = useMemo(() => state, Object.values(state));
return styled(useStyles(...style))(
<TabsContext.Provider value={value}>
<TabsState currentTabId={currentTabId}>
<tabs-box as="div" className={className}>
{tabs && (
<tabs as="div">
@@ -50,6 +40,6 @@ export function TabsBox({
{children}
</tab-panels>
</tabs-box>
</TabsContext.Provider>
</TabsState>
);
}
@@ -0,0 +1,40 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 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 { useEffect, useMemo } from 'react';
import { useTabState } from 'reakit/Tab';
import { TabsContext } from './TabsContext';
type Props = React.PropsWithChildren<{
selectedId?: string;
currentTabId?: string | null;
}>
export function TabsState({ selectedId, currentTabId, children }: Props) {
const state = useTabState({
selectedId: selectedId || currentTabId,
manual: true,
});
if (currentTabId) {
state.selectedId = currentTabId;
}
useEffect(() => {
if (!currentTabId) {
return;
}
state.select(currentTabId);
}, [currentTabId]); // hack currentId and selectedId not works
const value = useMemo(() => state, Object.values(state));
return (
<TabsContext.Provider value={value}>
{children}
</TabsContext.Provider>
);
}
@@ -1,6 +1,8 @@
export * from './Tab';
export * from './TabPanel';
export * from './TabsContext';
export * from './TabsState';
export * from './TabList';
export * from './ITab';
@@ -32,7 +32,7 @@ export const ConnectionForm = observer(function ConnectionForm({
const translate = useTranslate();
return styled(useStyles(formStyles))(
<>
<connection-form as='div'>
<connection-type as="div">
<Radio
name="type"
@@ -74,6 +74,6 @@ export const ConnectionForm = observer(function ConnectionForm({
</group>
)}
{!driver?.anonymousAccess && <CredentialsForm controller={controller}/>}
</>
</connection-form>
);
});
@@ -9,6 +9,10 @@
import { css } from 'reshadow';
export const formStyles = css`
connection-form {
flex-direction: column;
padding: 18px 24px;
}
connection-type {
padding: 12px;
}
@@ -7,13 +7,13 @@
*/
import { observer } from 'mobx-react';
import {
useTabState, Tab as BaseTab, TabList, TabPanel as BaseTabPanel,
} from 'reakit/Tab';
import { useState } from 'react';
import styled, { css } from 'reshadow';
import { DBDriver } from '@dbeaver/core/app';
import { SubmittingForm, ErrorMessage } from '@dbeaver/core/blocks';
import {
SubmittingForm, ErrorMessage, TabsState, TabList, Tab, TabTitle, TabPanel
} from '@dbeaver/core/blocks';
import { useController } from '@dbeaver/core/di';
import { CommonDialogWrapper } from '@dbeaver/core/dialogs';
import { useTranslate } from '@dbeaver/core/localization';
@@ -26,7 +26,7 @@ import { DriverProperties } from './DriverProperties/DriverProperties';
const styles = composes(
css`
BaseTab {
Tab {
composes: theme-ripple theme-background-secondary theme-text-on-secondary from global;
}
ErrorMessage {
@@ -52,39 +52,20 @@ const styles = composes(
flex-direction: column;
}
BaseTab, BaseTabPanel {
outline: none;
}
TabList {
box-sizing: border-box;
display: inline-flex;
width: 100%;
padding-left: 24px;
}
BaseTab {
Tab {
composes: theme-typography--body2 from global;
text-transform: uppercase;
padding: 12px 16px;
border-top: solid 2px transparent;
height: 48px;
font-weight: normal;
&:global([aria-selected='true']) {
border-top-color: #fd1d48;
&:before {
display: none;
}
&:global([aria-selected=true]) {
font-weight: normal !important;
}
&:not(:global([aria-selected='true'])) {
cursor: pointer;
background-color: transparent !important;
}
}
BaseTabPanel::first-child {
flex-direction: column;
padding: 18px 24px;
}
ErrorMessage {
position: sticky;
@@ -110,50 +91,54 @@ export const ConnectionFormDialog = observer(
}: ConnectionFormDialogProps) {
const translate = useTranslate();
const controller = useController(ConnectionFormDialogController, driver, onClose);
const tab = useTabState({
selectedId: 'options',
});
const [loadProperties, setLoadProperties] = useState(false);
return styled(useStyles(styles))(
<CommonDialogWrapper
title={title}
noBodyPadding
header={(
<TabList {...tab} aria-label="My tabs">
<BaseTab {...tab} type='button' stopId='options'>{translate('customConnection_options')}</BaseTab>
<BaseTab {...tab} type='button' stopId='driver_properties'>{translate('customConnection_properties')}</BaseTab>
</TabList>
)}
footer={(
<ConnectionFormDialogFooter
isConnecting={controller.isConnecting}
onConnectionTest={controller.onTestConnection}
onCreateConnection={controller.onCreateConnection}
onBack={onBack}
/>
)}
onReject={onClose}
>
<SubmittingForm onSubmit={controller.onCreateConnection}>
<BaseTabPanel {...tab} stopId='options'>
<ConnectionForm driver={driver} controller={controller} />
</BaseTabPanel>
<BaseTabPanel {...tab} stopId='driver_properties'>
<DriverProperties
driver={driver}
state={controller.config.properties}
isSelected={tab.selectedId === 'driver_properties'}
<TabsState selectedId='options'>
<CommonDialogWrapper
title={title}
noBodyPadding
header={(
<TabList>
<Tab tabId='options' >
<TabTitle title={translate('customConnection_options')} />
</Tab>
<Tab tabId='driver_properties' onOpen={() => setLoadProperties(true)} >
<TabTitle title={translate('customConnection_properties')} />
</Tab>
</TabList>
)}
footer={(
<ConnectionFormDialogFooter
isConnecting={controller.isConnecting}
onConnectionTest={controller.onTestConnection}
onCreateConnection={controller.onCreateConnection}
onBack={onBack}
/>
</BaseTabPanel>
</SubmittingForm>
{controller.error.responseMessage && (
<ErrorMessage
text={controller.error.responseMessage}
hasDetails={controller.error.hasDetails}
onShowDetails={controller.onShowDetails}
/>
)}
</CommonDialogWrapper>
)}
onReject={onClose}
>
<SubmittingForm onSubmit={controller.onCreateConnection}>
<TabPanel tabId='options'>
<ConnectionForm driver={driver} controller={controller} />
</TabPanel>
<TabPanel tabId='driver_properties'>
<DriverProperties
driver={driver}
state={controller.config.properties!}
loadProperties={loadProperties}
/>
</TabPanel>
</SubmittingForm>
{controller.error.responseMessage && (
<ErrorMessage
text={controller.error.responseMessage}
hasDetails={controller.error.hasDetails}
onShowDetails={controller.onShowDetails}
/>
)}
</CommonDialogWrapper>
</TabsState>
);
}
);
@@ -22,6 +22,7 @@ const styles = css`
display: flex;
flex: 1;
flex-direction: column;
overflow: auto;
}
`;
@@ -32,21 +33,21 @@ type DriverPropertyState = {
type DriverPropertiesProps = {
driver: DBDriver;
state: DriverPropertyState;
isSelected: boolean;
loadProperties: boolean;
}
export const DriverProperties = observer(function DriverProperties({
driver,
state,
isSelected,
loadProperties,
}: DriverPropertiesProps) {
const controller = useController(DriverPropertiesController, driver);
useEffect(() => {
if (isSelected) {
if (loadProperties) {
controller.loadDriverProperties();
}
}, [isSelected]);
}, [loadProperties]);
return styled(useStyles(styles))(
<properties as="div">