fix(core-connections): CB-511 connection credentials

This commit is contained in:
Wroud
2020-11-21 11:24:17 +03:00
parent 4568b3f0c5
commit 18098d2f75
13 changed files with 171 additions and 136 deletions
@@ -17,6 +17,7 @@ import { FormContext } from './FormContext';
import { isControlPresented } from './isControlPresented';
type BaseProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'name' | 'value'> & {
description?: string;
mod?: 'surface';
long?: boolean;
short?: boolean;
@@ -52,6 +53,7 @@ export const InputField: InputFieldType = observer(function InputField({
state,
children,
className,
description,
mod,
long,
short,
@@ -86,11 +88,16 @@ export const InputField: InputFieldType = observer(function InputField({
<input
{...rest}
name={name}
value={value}
value={value ?? ''}
onChange={handleChange}
{...use({ mod })}
required={required}
/>
{description && (
<field-description as='div'>
{description}
</field-description>
)}
</field>
);
});
@@ -139,7 +139,7 @@ export const Radio: RadioType = observer(function Radio({
type="radio"
id={id}
name={name}
value={value}
value={value ?? ''}
checked={checked}
onChange={handleChange}
/>
@@ -22,7 +22,7 @@ export const ShadowInput = React.forwardRef(function ShadowInput({
return (
<input
ref={ref}
value={children}
value={children ?? ''}
onChange={e => onChange?.(e.target.value)}
{...rest}
/>
@@ -79,7 +79,7 @@ export const Textarea: TextareaType = observer(function Textarea({
<field-label as='label'>{children}</field-label>
<textarea
{...rest}
value={value}
value={value ?? ''}
name={name}
onChange={handleChange}
{...use({ mod })}
@@ -40,7 +40,7 @@ export const baseFormControlStyles = css`
flex-basis: 100%;
padding-top: 8px;
padding-bottom: 8px;
padding-left: 150px;
padding-left: 180px;
}
field[|raw] {
@@ -20,7 +20,62 @@ import { Link } from '../Link';
import { TextPlaceholder } from '../TextPlaceholder';
import { formStyles } from './formStyles';
interface Props {
const RESERVED_KEYWORDS = ['no', 'off', 'new-password'];
interface RenderFieldProps {
property: ObjectPropertyInfo;
state: Record<string, any>;
autofillToken?: string;
disabled?: boolean;
readOnly?: boolean;
autoHide?: boolean;
onFocus?: (event: React.FocusEvent<HTMLInputElement>) => void;
}
const RenderField: React.FC<RenderFieldProps> = observer(function RenderField({
property,
state,
autofillToken = '',
disabled,
readOnly,
autoHide,
onFocus,
}) {
const href = property.features.includes('href');
const password = property.features.includes('password');
let description: string | undefined;
if (href) {
return (
<FormFieldDescription label={property.displayName} raw>
<Link href={property.value} target='_blank' rel='noopener noreferrer'>{property.description}</Link>
</FormFieldDescription>
);
}
if (password && property.value) {
description = 'Password saved';
}
return (
<InputField
type={password ? 'password' : 'text'}
name={property.id!}
state={state}
description={description}
disabled={disabled}
readOnly={readOnly}
autoHide={autoHide}
autoComplete={RESERVED_KEYWORDS.includes(autofillToken) ? autofillToken : `${autofillToken} ${property.id}`}
mod='surface'
onFocus={onFocus}
>
{property.displayName}
</InputField>
);
});
interface ObjectPropertyFormProps {
properties: ObjectPropertyInfo[] | undefined;
credentials: Record<string, string | number>;
autofillToken?: string;
@@ -31,9 +86,7 @@ interface Props {
onFocus?: (name: string) => void;
}
const RESERVED_KEYWORDS = ['no', 'off', 'new-password'];
export const ObjectPropertyInfoForm: React.FC<Props> = observer(function ObjectPropertyInfoForm({
export const ObjectPropertyInfoForm: React.FC<ObjectPropertyFormProps> = observer(function ObjectPropertyInfoForm({
properties,
credentials,
autofillToken = '',
@@ -49,16 +102,7 @@ export const ObjectPropertyInfoForm: React.FC<Props> = observer(function ObjectP
if (onFocus) {
onFocus(e.target.name);
}
if (e.target.type !== 'password') {
return;
}
const property = properties?.find(property => property.id === e.target.name);
if (property?.value === e.target.value) {
credentials[e.target.name] = '';
}
}, [properties, credentials, onFocus]);
}, [onFocus]);
if (!properties || properties.length === 0) {
return <TextPlaceholder>Properties empty</TextPlaceholder>;
@@ -68,25 +112,15 @@ export const ObjectPropertyInfoForm: React.FC<Props> = observer(function ObjectP
<form-body as='div' className={className}>
{properties.map(property => (
<FormGroup key={property.id}>
{property.features.includes('href') ? (
<FormFieldDescription label={property.displayName} raw>
<Link href={property.value} target='_blank' rel='noopener noreferrer'>{property.description}</Link>
</FormFieldDescription>
) : (
<InputField
type={property.features.includes('password') ? 'password' : 'text'}
name={property.id!}
state={credentials}
disabled={disabled}
readOnly={readOnly}
autoHide={autoHide}
autoComplete={RESERVED_KEYWORDS.includes(autofillToken) ? autofillToken : `${autofillToken} ${property.id}`}
mod='surface'
onFocus={handleFocus}
>
{property.displayName}
</InputField>
)}
<RenderField
property={property}
state={credentials}
autofillToken={autofillToken}
disabled={disabled}
readOnly={readOnly}
autoHide={autoHide}
onFocus={handleFocus}
/>
</FormGroup>
))}
</form-body>
@@ -23,7 +23,6 @@ import { IConnectionFormModel } from './IConnectionFormModel';
export class ConnectionFormController
implements IInitializableController {
@observable isSaving: boolean;
@observable readonly metadata: Map<string, any>;
readonly afterSave: IExecutor<string>;
@@ -52,7 +51,6 @@ implements IInitializableController {
private dbDriverResource: DBDriverResource
) {
this.isSaving = false;
this.metadata = new Map<string, any>();
this.afterSave = new Executor();
}
@@ -141,7 +139,13 @@ implements IInitializableController {
return true;
}
for (const property of this.model.connection.authProperties) {
if (property.value !== null && this.model.credentials[property.id!] !== property.value) {
const value = this.model.credentials[property.id!];
if (property.features.includes('password')) {
if (value) {
return true;
}
} else if (value !== property.value) {
return true;
}
}
@@ -139,10 +139,9 @@ export const Options = observer(function Options({
</FormGroup>
<ObjectPropertyInfoForm
autofillToken='new-password'
properties={controller.authModel.properties}
properties={controller.properties}
credentials={model.credentials}
disabled={disabled}
onFocus={controller.resetPassword}
/>
<FormGroup>
<FieldCheckbox
@@ -10,7 +10,7 @@ import { action, computed } from 'mobx';
import { injectable, IInitializableController } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { DatabaseAuthModel } from '@cloudbeaver/core-sdk';
import { DatabaseAuthModel, ObjectPropertyInfo } from '@cloudbeaver/core-sdk';
import { DatabaseAuthModelsResource } from '../../../../DatabaseAuthModelsResource';
import { DBDriver, DBDriverResource } from '../../../../DBDriverResource';
@@ -39,6 +39,13 @@ implements IInitializableController {
return this.dbAuthModelsResource.isLoading();
}
@computed get properties(): ObjectPropertyInfo[] {
if (this.model.connection.authProperties) {
return this.model.connection.authProperties;
}
return this.authModel?.properties || [];
}
private model!: IConnectionFormModel;
private nameTemplate = /^.*?\s(|\(.*?\)\s)connection$/;
@@ -61,39 +68,14 @@ implements IInitializableController {
onFormChange = (value?: unknown, name?: string): void => {
this.updateName(name);
this.resetPassword(name || '');
};
@action
private setDefaults(prevDriverId: string | null) {
this.setDefaultParameters(prevDriverId);
this.cleanCredentials();
this.model.connection.properties = {};
this.model.connection.authModel = this.driver?.defaultAuthModel;
this.cleanCredentials();
}
resetPassword = (name: string): void => {
const passwordProperty = this.model.connection.authProperties.find(property => property.features.includes('password'));
if (passwordProperty && (this.isCredentialsChanged() || passwordProperty.id === name)) {
if (this.model.credentials[passwordProperty.id!] === passwordProperty.value) {
this.model.credentials[passwordProperty.id!] = '';
this.model.connection.saveCredentials = false;
}
}
};
private isCredentialsChanged() {
if (!Object.keys(this.model.credentials).length) {
return false;
}
for (const property of this.model.connection.authProperties) {
// commented because of case when name changed, password should be reset
if (/* property.value !== null && */this.model.credentials[property.id!] !== property.value) {
return true;
}
}
return false;
}
private cleanCredentials() {
@@ -160,25 +142,10 @@ implements IInitializableController {
private async loadDrivers() {
try {
await this.dbDriverResource.loadAll();
await this.dbAuthModelsResource.load(
this.model.connection.authModel || this.driver!.defaultAuthModel
);
this.setDefaultParameters();
if (!this.driver || this.driver.anonymousAccess) {
return;
}
try {
await this.dbAuthModelsResource.load(
this.model.connection?.authModel || this.driver.defaultAuthModel
);
if (this.authModel) {
for (const property of this.model.connection.authProperties) {
this.model.credentials[property.id!] = property.value;
}
}
} catch (exception) {
this.notificationService.logException(exception, 'Can\'t load driver auth model');
}
} catch (exception) {
this.notificationService.logException(exception, 'Can\'t load drivers');
}
@@ -187,33 +154,17 @@ implements IInitializableController {
private async loadDriver(driverId: string | null, prev: string | null) {
if (!driverId) {
this.model.connection.authModel = undefined;
this.cleanCredentials();
return;
}
try {
await this.dbDriverResource.load(driverId);
await this.dbAuthModelsResource.load(
this.model.connection.authModel || this.driver!.defaultAuthModel
);
this.setDefaults(prev);
} catch (exception) {
this.notificationService.logException(exception, `Can't load driver ${driverId}`);
}
if (!this.driver || this.driver.anonymousAccess) {
return;
}
try {
await this.dbAuthModelsResource.load(
this.model.connection?.authModel || this.driver.defaultAuthModel
);
if (this.authModel) {
for (const property of this.model.connection.authProperties) {
this.model.credentials[property.id!] = property.value;
}
}
} catch (exception) {
this.notificationService.logException(exception, 'Can\'t load driver auth model');
}
}
}
@@ -12,8 +12,9 @@ import {
injectable, IInitializableController, IDestructibleController
} from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { GQLErrorCatcher, AdminConnectionGrantInfo } from '@cloudbeaver/core-sdk';
import { GQLErrorCatcher, AdminConnectionGrantInfo, ResourceKeyUtils, ResourceKey } from '@cloudbeaver/core-sdk';
import { DatabaseAuthModelsResource } from '../../../DatabaseAuthModelsResource';
import { DBDriverResource } from '../../../DBDriverResource';
import { AdminConnection, ConnectionsResource } from '../../ConnectionsResource';
@@ -51,26 +52,68 @@ implements IInitializableController, IDestructibleController {
constructor(
private connectionsResource: ConnectionsResource,
private notificationService: NotificationService,
private dbDriverResource: DBDriverResource
) { }
private dbDriverResource: DBDriverResource,
private dbAuthModelsResource: DatabaseAuthModelsResource
) {
this.updateConnectionInfo = this.updateConnectionInfo.bind(this);
}
init(id: string) {
async init(id: string): Promise<void> {
this.connectionId = id;
this.loadConnectionInfo();
await this.loadConnectionInfo();
this.connectionsResource.onItemAdd.addHandler(this.updateConnectionInfo);
}
destruct(): void {
this.connectionsResource.onItemAdd.removeHandler(this.updateConnectionInfo);
}
private async loadConnectionInfo() {
this.isLoading = true;
try {
// we create a copy to protect the current value from mutation
this.connection = JSON.parse(JSON.stringify(await this.connectionsResource.load(this.connectionId)));
await this.connectionsResource.load(this.connectionId);
await this.updateConnectionInfo(this.connectionId);
} catch (exception) {
this.notificationService.logException(exception, "Can't load ConnectionInfo", `Can't load ConnectionInfo ${this.connectionId}`);
} finally {
this.isLoading = false;
}
}
private async updateConnectionInfo(key: ResourceKey<string>) {
if (!ResourceKeyUtils.includes(key, this.connectionId)) {
return;
}
this.connection = JSON.parse(JSON.stringify(await this.connectionsResource.load(this.connectionId)));
await this.updateCredentials();
}
private cleanCredentials() {
for (const property of Object.keys(this.credentials)) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete this.credentials[property];
}
}
private async updateCredentials() {
this.cleanCredentials();
if (!this.driver || this.driver.anonymousAccess || !this.connection) {
return;
}
try {
await this.dbAuthModelsResource.load(
this.connection.authModel || this.driver.defaultAuthModel
);
for (const property of this.connection.authProperties) {
if (!property.features.includes('password')) {
this.credentials[property.id!] = property.value;
}
}
} catch (exception) {
this.notificationService.logException(exception, 'Can\'t load driver auth model');
}
}
}
@@ -25,9 +25,7 @@ fragment AdminConnection on ConnectionInfo {
authNeeded
authModel
authProperties {
id
value
features
...UserConnectionAuthProperties
}
features
+18 -19
View File
@@ -1167,7 +1167,7 @@ export interface NavGetStructContainersQuery { navGetStructContainers: { catalog
export type AdminConnectionFragment = (
Pick<ConnectionInfo, 'id' | 'name' | 'description' | 'driverId' | 'template' | 'connected' | 'useUrl' | 'readOnly' | 'saveCredentials' | 'host' | 'port' | 'databaseName' | 'url' | 'properties' | 'features' | 'authNeeded' | 'authModel' | 'supportedDataFormats'>
& { origin: ObjectOriginInfoFragment; authProperties: Array<Pick<ObjectPropertyInfo, 'id' | 'value' | 'features'>> }
& { origin: ObjectOriginInfoFragment; authProperties: UserConnectionAuthPropertiesFragment[] }
);
export type AdminUserInfoFragment = (
@@ -1455,6 +1455,20 @@ export const ObjectOriginInfoFragmentDoc = `
icon
}
`;
export const UserConnectionAuthPropertiesFragmentDoc = `
fragment UserConnectionAuthProperties on ObjectPropertyInfo {
id
displayName
description
category
dataType
value
validValues
defaultValue
features
order
}
`;
export const AdminConnectionFragmentDoc = `
fragment AdminConnection on ConnectionInfo {
id
@@ -1478,14 +1492,13 @@ export const AdminConnectionFragmentDoc = `
authNeeded
authModel
authProperties {
id
value
features
...UserConnectionAuthProperties
}
features
supportedDataFormats
}
${ObjectOriginInfoFragmentDoc}`;
${ObjectOriginInfoFragmentDoc}
${UserConnectionAuthPropertiesFragmentDoc}`;
export const AdminUserInfoFragmentDoc = `
fragment AdminUserInfo on AdminUserInfo {
userId
@@ -1548,20 +1561,6 @@ export const UserConnectionFragmentDoc = `
supportedDataFormats
}
`;
export const UserConnectionAuthPropertiesFragmentDoc = `
fragment UserConnectionAuthProperties on ObjectPropertyInfo {
id
displayName
description
category
dataType
value
validValues
defaultValue
features
order
}
`;
export const AsyncTaskCancelDocument = `
mutation asyncTaskCancel($taskId: String!) {
result: asyncTaskCancel(id: $taskId)
@@ -51,7 +51,7 @@ implements IInitializableController, IDestructibleController {
try {
// we create a copy to protect the current value from mutation
await this.usersResource.load(this.userId);
this.updateUser(this.userId);
await this.updateUser(this.userId);
} catch (exception) {
this.notificationService.logException(exception, `Can't load user ${this.userId}`);
}