fix: secret saved indicator on ui for datasource forms (#18531)

## Description

Secret saved indicator on ui exists if the datasource field has a
`valueExistPath` and server sends back the boolean value for the
specific field in `secretExists` key.

The UI would appear as follows : 

#### When the password is saved and there exists a key `valueExistPath`
for `Password` field and the value in `secretExists` is true then
- When password field is not focused. An overlay indicating the password
shows up.
<img width="575" alt="Screenshot 2022-11-28 at 8 58 44 PM"
src="https://user-images.githubusercontent.com/7565635/204317024-be22127b-adf4-4914-9180-804ebe6b482a.png">

- When the password field is focused. The overlay goes away.
<img width="588" alt="Screenshot 2022-11-28 at 8 58 51 PM"
src="https://user-images.githubusercontent.com/7565635/204317400-9d601230-5493-40c0-ac66-21112d0d98ca.png">

TL;DR

Fixes #14783

Media
[Loom Video of 4
sec](https://www.loom.com/share/ba30b9674d754bf4a0c2704eef69008d)


## Type of change

- New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

- Manual

### Test Plan
> Add Testsmith test cases links that relate to this PR

### Issues raised during DP testing
> Link issues raised during DP testing for better visiblity and tracking
(copy link from comments dropped on this PR)


## Checklist:
### Dev activity
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] PR is being merged under a feature flag


### QA activity:
- [ ] Test plan has been approved by relevant developers
- [ ] Test plan has been peer reviewed by QA
- [ ] Cypress test cases have been added and approved by either SDET or
manual QA
- [ ] Organized project review call with relevant stakeholders after
Round 1/2 of QA
- [ ] Added Test Plan Approved label after reveiwing all Cypress test

Co-authored-by: “sneha122” <“sneha@appsmith.com”>
This commit is contained in:
Aman Agarwal 2023-01-20 19:33:42 +05:30 committed by GitHub
parent 645e35a8e1
commit 276e1e6c4b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 272 additions and 182 deletions

View File

@ -83,6 +83,7 @@ export interface ControlData {
disabled?: boolean; disabled?: boolean;
staticDependencyPathList?: string[]; staticDependencyPathList?: string[];
validator?: (value: string) => { isValid: boolean; message: string }; validator?: (value: string) => { isValid: boolean; message: string };
isSecretExistsPath?: string;
} }
export type FormConfigType = Omit<ControlData, "configProperty"> & { export type FormConfigType = Omit<ControlData, "configProperty"> & {
configProperty?: string; configProperty?: string;

View File

@ -2,6 +2,7 @@ import React from "react";
import BaseControl, { ControlProps } from "./BaseControl"; import BaseControl, { ControlProps } from "./BaseControl";
import { ControlType } from "constants/PropertyControlConstants"; import { ControlType } from "constants/PropertyControlConstants";
import { TextInput } from "design-system"; import { TextInput } from "design-system";
import { AppState } from "@appsmith/reducers";
import { Colors } from "constants/Colors"; import { Colors } from "constants/Colors";
import styled from "styled-components"; import styled from "styled-components";
import { InputType } from "components/constants"; import { InputType } from "components/constants";
@ -9,7 +10,9 @@ import {
Field, Field,
WrappedFieldMetaProps, WrappedFieldMetaProps,
WrappedFieldInputProps, WrappedFieldInputProps,
formValueSelector,
} from "redux-form"; } from "redux-form";
import { connect } from "react-redux";
export const StyledInfo = styled.span` export const StyledInfo = styled.span`
font-weight: normal; font-weight: normal;
@ -19,42 +22,34 @@ export const StyledInfo = styled.span`
margin-left: 1px; margin-left: 1px;
`; `;
export function InputText(props: { const FieldWrapper = styled.div`
label: string; width: 35vw;
value: string; position: relative;
isValid: boolean; `;
subtitle?: string;
validationMessage?: string;
placeholder?: string;
dataType?: string;
isRequired?: boolean;
name: string;
encrypted?: boolean;
disabled?: boolean;
customStyles?: Record<string, any>;
validator?: (value: string) => { isValid: boolean; message: string };
}) {
const { dataType, disabled, name, placeholder } = props;
return ( const SecretDisplayIndicator = styled.input`
<div data-cy={name} style={{ width: "35vw", ...props.customStyles }}> position: absolute;
<Field top: 0;
component={renderComponent} left: 0;
datatype={dataType} width: 100%;
disabled={disabled || false} height: 100%;
placeholder={placeholder} display: flex;
{...props} align-items: center;
asyncControl padding: 0px var(--ads-spaces-6);
/> z-index: 1;
</div> cursor: text;
); border: none;
} background: none;
`;
const PASSWORD_EXISTS_INDICATOR = "······";
function renderComponent( function renderComponent(
props: { props: {
placeholder: string; placeholder: string;
dataType?: InputType; dataType?: InputType;
disabled?: boolean; disabled?: boolean;
reference: any;
validator?: (value: string) => { isValid: boolean; message: string }; validator?: (value: string) => { isValid: boolean; message: string };
} & { } & {
meta: Partial<WrappedFieldMetaProps>; meta: Partial<WrappedFieldMetaProps>;
@ -68,6 +63,7 @@ function renderComponent(
name={props.input?.name} name={props.input?.name}
onChange={props.input.onChange} onChange={props.input.onChange}
placeholder={props.placeholder} placeholder={props.placeholder}
ref={props.reference}
value={props.input.value} value={props.input.value}
{...props.input} {...props.input}
validator={props.validator} validator={props.validator}
@ -75,10 +71,59 @@ function renderComponent(
/> />
); );
} }
class InputTextControl extends BaseControl<InputControlProps> { class InputTextControl extends BaseControl<InputControlProps> {
fieldRef: any;
state = {
secretDisplayVisible: false,
};
constructor(props: InputControlProps) {
super(props);
this.fieldRef = React.createRef();
}
onClickSecretDisplayIndicator = () => {
if (!this.state.secretDisplayVisible) return;
this.setState({
secretDisplayVisible: false,
});
if (this.fieldRef.current) this.fieldRef.current?.focus();
};
checkForSecretOverlayIndicator = () => {
return (
this.props.dataType === "PASSWORD" &&
this.props.isSecretExistsPath &&
this.props.isSecretExistsData
);
};
onBlur = () => {
if (
this.checkForSecretOverlayIndicator() &&
this.fieldRef.current?.value?.length === 0
) {
this.setState({
secretDisplayVisible: true,
});
}
};
componentDidMount() {
if (this.checkForSecretOverlayIndicator()) {
this.setState({
secretDisplayVisible: true,
});
}
}
render() { render() {
const { const {
configProperty, configProperty,
customStyles,
dataType, dataType,
disabled, disabled,
encrypted, encrypted,
@ -92,19 +137,34 @@ class InputTextControl extends BaseControl<InputControlProps> {
} = this.props; } = this.props;
return ( return (
<InputText <FieldWrapper data-cy={configProperty} style={customStyles || {}}>
dataType={this.getType(dataType)} {this.state.secretDisplayVisible && (
disabled={disabled} <SecretDisplayIndicator
encrypted={encrypted} onClick={this.onClickSecretDisplayIndicator}
isValid={isValid} onFocus={this.onClickSecretDisplayIndicator}
label={label} type="password"
name={configProperty} value={PASSWORD_EXISTS_INDICATOR}
placeholder={placeholderText} />
subtitle={subtitle} )}
validationMessage={validationMessage} <Field
validator={validator} asyncControl
value={propertyValue} component={renderComponent}
/> dataType={this.getType(dataType)}
disabled={disabled || false}
encrypted={encrypted}
isValid={isValid}
label={label}
name={configProperty}
onBlur={this.onBlur}
onFocus={this.onClickSecretDisplayIndicator}
placeholder={this.state.secretDisplayVisible ? "" : placeholderText}
reference={this.fieldRef}
subtitle={subtitle}
validationMessage={validationMessage}
validator={validator}
value={propertyValue}
/>
</FieldWrapper>
); );
} }
@ -145,6 +205,18 @@ export interface InputControlProps extends ControlProps {
encrypted?: boolean; encrypted?: boolean;
disabled?: boolean; disabled?: boolean;
validator?: (value: string) => { isValid: boolean; message: string }; validator?: (value: string) => { isValid: boolean; message: string };
isSecretExistsData?: boolean;
} }
export default InputTextControl; const mapStateToProps = (state: AppState, props: InputControlProps) => {
const valueSelector = formValueSelector(props.formName);
let isSecretExistsData;
if (props.isSecretExistsPath) {
isSecretExistsData = valueSelector(state, props.isSecretExistsPath);
}
return {
isSecretExistsData,
};
};
export default connect(mapStateToProps)(InputTextControl);

View File

@ -91,6 +91,7 @@ export interface Basic {
authenticationType: AuthType.basic; authenticationType: AuthType.basic;
username: string; username: string;
password: string; password: string;
secretExists?: Record<string, boolean>;
} }
export interface ApiKey { export interface ApiKey {

View File

@ -23,6 +23,7 @@ export interface DatasourceAuthentication {
bearerToken?: string; bearerToken?: string;
authenticationStatus?: string; authenticationStatus?: string;
authenticationType?: string; authenticationType?: string;
secretExists?: Record<string, boolean>;
} }
export interface DatasourceColumns { export interface DatasourceColumns {

View File

@ -452,15 +452,15 @@ class DatasourceRestAPIEditor extends React.Component<
return ( return (
<section data-cy="section-General" data-replay-id="section-General"> <section data-cy="section-General" data-replay-id="section-General">
<FormInputContainer data-replay-id={btoa("url")}> <FormInputContainer data-replay-id={btoa("url")}>
{this.renderInputTextControlViaFormControl( {this.renderInputTextControlViaFormControl({
"url", configProperty: "url",
"URL", label: "URL",
"https://example.com", placeholderText: "https://example.com",
"TEXT", dataType: "TEXT",
false, encrypted: false,
true, isRequired: true,
this.urlValidator, fieldValidator: this.urlValidator,
)} })}
</FormInputContainer> </FormInputContainer>
<FormInputContainer <FormInputContainer
className="t--headers-array" className="t--headers-array"
@ -502,14 +502,14 @@ class DatasourceRestAPIEditor extends React.Component<
</FormInputContainer> </FormInputContainer>
{formData.isSendSessionEnabled && ( {formData.isSendSessionEnabled && (
<FormInputContainer data-replay-id={btoa("sessionSignatureKey")}> <FormInputContainer data-replay-id={btoa("sessionSignatureKey")}>
{this.renderInputTextControlViaFormControl( {this.renderInputTextControlViaFormControl({
"sessionSignatureKey", configProperty: "sessionSignatureKey",
"Session Details Signature Key", label: "Session Details Signature Key",
"", placeholderText: "",
"TEXT", dataType: "TEXT",
false, encrypted: false,
false, isRequired: false,
)} })}
</FormInputContainer> </FormInputContainer>
)} )}
<FormInputContainer data-replay-id={btoa("authType")}> <FormInputContainer data-replay-id={btoa("authType")}>
@ -591,24 +591,24 @@ class DatasourceRestAPIEditor extends React.Component<
return ( return (
<> <>
<FormInputContainer data-replay-id={btoa("authentication.label")}> <FormInputContainer data-replay-id={btoa("authentication.label")}>
{this.renderInputTextControlViaFormControl( {this.renderInputTextControlViaFormControl({
"authentication.label", configProperty: "authentication.label",
"Key", label: "Key",
"api_key", placeholderText: "api_key",
"TEXT", dataType: "TEXT",
false, encrypted: false,
false, isRequired: false,
)} })}
</FormInputContainer> </FormInputContainer>
<FormInputContainer> <FormInputContainer>
{this.renderInputTextControlViaFormControl( {this.renderInputTextControlViaFormControl({
"authentication.value", configProperty: "authentication.value",
"Value", label: "Value",
"value", placeholderText: "value",
"TEXT", dataType: "TEXT",
true, encrypted: true,
false, isRequired: false,
)} })}
</FormInputContainer> </FormInputContainer>
<FormInputContainer> <FormInputContainer>
{this.renderDropdownControlViaFormControl( {this.renderDropdownControlViaFormControl(
@ -633,14 +633,14 @@ class DatasourceRestAPIEditor extends React.Component<
<FormInputContainer <FormInputContainer
data-replay-id={btoa("authentication.headerPrefix")} data-replay-id={btoa("authentication.headerPrefix")}
> >
{this.renderInputTextControlViaFormControl( {this.renderInputTextControlViaFormControl({
"authentication.headerPrefix", configProperty: "authentication.headerPrefix",
"Header Prefix", label: "Header Prefix",
"eg: Bearer ", placeholderText: "eg: Bearer ",
"TEXT", dataType: "TEXT",
false, encrypted: false,
false, isRequired: false,
)} })}
</FormInputContainer> </FormInputContainer>
)} )}
</> </>
@ -650,14 +650,14 @@ class DatasourceRestAPIEditor extends React.Component<
renderBearerToken = () => { renderBearerToken = () => {
return ( return (
<FormInputContainer data-replay-id={btoa("authentication.bearerToken")}> <FormInputContainer data-replay-id={btoa("authentication.bearerToken")}>
{this.renderInputTextControlViaFormControl( {this.renderInputTextControlViaFormControl({
"authentication.bearerToken", configProperty: "authentication.bearerToken",
"Bearer Token", label: "Bearer Token",
"Bearer Token", placeholderText: "Bearer Token",
"TEXT", dataType: "TEXT",
true, encrypted: true,
false, isRequired: false,
)} })}
</FormInputContainer> </FormInputContainer>
); );
}; };
@ -666,24 +666,25 @@ class DatasourceRestAPIEditor extends React.Component<
return ( return (
<> <>
<FormInputContainer data-replay-id={btoa("authentication.username")}> <FormInputContainer data-replay-id={btoa("authentication.username")}>
{this.renderInputTextControlViaFormControl( {this.renderInputTextControlViaFormControl({
"authentication.username", configProperty: "authentication.username",
"Username", label: "Username",
"Username", placeholderText: "Username",
"TEXT", dataType: "TEXT",
false, encrypted: false,
false, isRequired: false,
)} })}
</FormInputContainer> </FormInputContainer>
<FormInputContainer data-replay-id={btoa("authentication.password")}> <FormInputContainer data-replay-id={btoa("authentication.password")}>
{this.renderInputTextControlViaFormControl( {this.renderInputTextControlViaFormControl({
"authentication.password", configProperty: "authentication.password",
"Password", label: "Password",
"Password", placeholderText: "Password",
"PASSWORD", dataType: "PASSWORD",
true, encrypted: true,
false, isRequired: false,
)} isSecretExistsPath: "authentication.secretExists.password",
})}
</FormInputContainer> </FormInputContainer>
</> </>
); );
@ -757,60 +758,61 @@ class DatasourceRestAPIEditor extends React.Component<
<FormInputContainer <FormInputContainer
data-replay-id={btoa("authentication.headerPrefix")} data-replay-id={btoa("authentication.headerPrefix")}
> >
{this.renderInputTextControlViaFormControl( {this.renderInputTextControlViaFormControl({
"authentication.headerPrefix", configProperty: "authentication.headerPrefix",
"Header Prefix", label: "Header Prefix",
"eg: Bearer ", placeholderText: "eg: Bearer ",
"TEXT", dataType: "TEXT",
false, encrypted: false,
false, isRequired: false,
)} })}
</FormInputContainer> </FormInputContainer>
)} )}
<FormInputContainer <FormInputContainer
data-replay-id={btoa("authentication.accessTokenUrl")} data-replay-id={btoa("authentication.accessTokenUrl")}
> >
{this.renderInputTextControlViaFormControl( {this.renderInputTextControlViaFormControl({
"authentication.accessTokenUrl", configProperty: "authentication.accessTokenUrl",
"Access Token URL", label: "Access Token URL",
"https://example.com/login/oauth/access_token", placeholderText: "https://example.com/login/oauth/access_token",
"TEXT", dataType: "TEXT",
false, encrypted: false,
false, isRequired: false,
this.urlValidator, fieldValidator: this.urlValidator,
)} })}
</FormInputContainer> </FormInputContainer>
<FormInputContainer data-replay-id={btoa("authentication.clientId")}> <FormInputContainer data-replay-id={btoa("authentication.clientId")}>
{this.renderInputTextControlViaFormControl( {this.renderInputTextControlViaFormControl({
"authentication.clientId", configProperty: "authentication.clientId",
"Client ID", label: "Client ID",
"Client ID", placeholderText: "Client ID",
"TEXT", dataType: "TEXT",
false, encrypted: false,
false, isRequired: false,
)} })}
</FormInputContainer> </FormInputContainer>
<FormInputContainer <FormInputContainer
data-replay-id={btoa("authentication.clientSecret")} data-replay-id={btoa("authentication.clientSecret")}
> >
{this.renderInputTextControlViaFormControl( {this.renderInputTextControlViaFormControl({
"authentication.clientSecret", configProperty: "authentication.clientSecret",
"Client Secret", label: "Client Secret",
"Client Secret", placeholderText: "Client Secret",
"PASSWORD", dataType: "PASSWORD",
true, encrypted: true,
false, isRequired: false,
)} isSecretExistsPath: "authentication.secretExists.clientSecret",
})}
</FormInputContainer> </FormInputContainer>
<FormInputContainer data-replay-id={btoa("authentication.scopeString")}> <FormInputContainer data-replay-id={btoa("authentication.scopeString")}>
{this.renderInputTextControlViaFormControl( {this.renderInputTextControlViaFormControl({
"authentication.scopeString", configProperty: "authentication.scopeString",
"Scope(s)", label: "Scope(s)",
"e.g. read, write", placeholderText: "e.g. read, write",
"TEXT", dataType: "TEXT",
false, encrypted: false,
false, isRequired: false,
)} })}
</FormInputContainer> </FormInputContainer>
<FormInputContainer <FormInputContainer
data-replay-id={btoa("authentication.isAuthorizationHeader")} data-replay-id={btoa("authentication.isAuthorizationHeader")}
@ -933,24 +935,24 @@ class DatasourceRestAPIEditor extends React.Component<
return ( return (
<> <>
<FormInputContainer data-replay-id={btoa("authentication.audience")}> <FormInputContainer data-replay-id={btoa("authentication.audience")}>
{this.renderInputTextControlViaFormControl( {this.renderInputTextControlViaFormControl({
"authentication.audience", configProperty: "authentication.audience",
"Audience", label: "Audience",
"https://example.com/oauth/audience", placeholderText: "https://example.com/oauth/audience",
"TEXT", dataType: "TEXT",
false, encrypted: false,
false, isRequired: false,
)} })}
</FormInputContainer> </FormInputContainer>
<FormInputContainer data-replay-id={btoa("authentication.resource")}> <FormInputContainer data-replay-id={btoa("authentication.resource")}>
{this.renderInputTextControlViaFormControl( {this.renderInputTextControlViaFormControl({
"authentication.resource", configProperty: "authentication.resource",
"Resource", label: "Resource",
"https://example.com/oauth/resource", placeholderText: "https://example.com/oauth/resource",
"TEXT", dataType: "TEXT",
false, encrypted: false,
false, isRequired: false,
)} })}
</FormInputContainer> </FormInputContainer>
</> </>
); );
@ -976,14 +978,14 @@ class DatasourceRestAPIEditor extends React.Component<
<FormInputContainer <FormInputContainer
data-replay-id={btoa("authentication.authorizationUrl")} data-replay-id={btoa("authentication.authorizationUrl")}
> >
{this.renderInputTextControlViaFormControl( {this.renderInputTextControlViaFormControl({
"authentication.authorizationUrl", configProperty: "authentication.authorizationUrl",
"Authorization URL", label: "Authorization URL",
"https://example.com/login/oauth/authorize", placeholderText: "https://example.com/login/oauth/authorize",
"TEXT", dataType: "TEXT",
false, encrypted: false,
false, isRequired: false,
)} })}
</FormInputContainer> </FormInputContainer>
<FormInputContainer> <FormInputContainer>
<div style={{ width: "20vw" }}> <div style={{ width: "20vw" }}>
@ -1016,15 +1018,25 @@ class DatasourceRestAPIEditor extends React.Component<
// All components in formControls must be rendered via FormControl. // All components in formControls must be rendered via FormControl.
// FormControl is the common wrapper for all formcontrol components and contains common elements i.e. label, subtitle, helpertext // FormControl is the common wrapper for all formcontrol components and contains common elements i.e. label, subtitle, helpertext
renderInputTextControlViaFormControl( renderInputTextControlViaFormControl({
configProperty: string, configProperty,
label: string, dataType,
placeholderText: string, encrypted,
dataType: "TEXT" | "PASSWORD" | "NUMBER", fieldValidator,
encrypted: boolean, isRequired,
isRequired: boolean, isSecretExistsPath,
fieldValidator?: (value: string) => { isValid: boolean; message: string }, label,
) { placeholderText,
}: {
configProperty: string;
label: string;
placeholderText: string;
dataType: "TEXT" | "PASSWORD" | "NUMBER";
encrypted: boolean;
isRequired: boolean;
fieldValidator?: (value: string) => { isValid: boolean; message: string };
isSecretExistsPath?: string;
}) {
return ( return (
<FormControl <FormControl
config={{ config={{
@ -1040,6 +1052,7 @@ class DatasourceRestAPIEditor extends React.Component<
placeholderText: placeholderText, placeholderText: placeholderText,
formName: DATASOURCE_REST_API_FORM, formName: DATASOURCE_REST_API_FORM,
validator: fieldValidator, validator: fieldValidator,
isSecretExistsPath,
}} }}
formName={DATASOURCE_REST_API_FORM} formName={DATASOURCE_REST_API_FORM}
multipleConfig={[]} multipleConfig={[]}

View File

@ -133,6 +133,7 @@ const formToDatasourceAuthentication = (
authenticationType: AuthType.basic, authenticationType: AuthType.basic,
username: authentication.username, username: authentication.username,
password: authentication.password, password: authentication.password,
secretExists: authentication.secretExists,
}; };
return basic; return basic;
} }
@ -224,6 +225,7 @@ const datasourceToFormAuthentication = (
authenticationType: AuthType.basic, authenticationType: AuthType.basic,
username: authentication.username || "", username: authentication.username || "",
password: authentication.password || "", password: authentication.password || "",
secretExists: authentication.secretExists,
}; };
return basic; return basic;
} }