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;
staticDependencyPathList?: string[];
validator?: (value: string) => { isValid: boolean; message: string };
isSecretExistsPath?: string;
}
export type FormConfigType = Omit<ControlData, "configProperty"> & {
configProperty?: string;

View File

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

View File

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

View File

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

View File

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