From 276e1e6c4bae78be3ad1dc94dd4d2dd77e128f7e Mon Sep 17 00:00:00 2001 From: Aman Agarwal Date: Fri, 20 Jan 2023 19:33:42 +0530 Subject: [PATCH] fix: secret saved indicator on ui for datasource forms (#18531) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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. Screenshot 2022-11-28 at 8 58 44 PM - When the password field is focused. The overlay goes away. Screenshot 2022-11-28 at 8 58 51 PM 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”> --- .../components/formControls/BaseControl.tsx | 1 + .../formControls/InputTextControl.tsx | 158 +++++++--- .../src/entities/Datasource/RestAPIForm.ts | 1 + app/client/src/entities/Datasource/index.ts | 1 + .../RestAPIDatasourceForm.tsx | 291 +++++++++--------- .../RestAPIDatasourceFormTransformer.ts | 2 + 6 files changed, 272 insertions(+), 182 deletions(-) diff --git a/app/client/src/components/formControls/BaseControl.tsx b/app/client/src/components/formControls/BaseControl.tsx index d0f1f7bf82..d23c417c26 100644 --- a/app/client/src/components/formControls/BaseControl.tsx +++ b/app/client/src/components/formControls/BaseControl.tsx @@ -83,6 +83,7 @@ export interface ControlData { disabled?: boolean; staticDependencyPathList?: string[]; validator?: (value: string) => { isValid: boolean; message: string }; + isSecretExistsPath?: string; } export type FormConfigType = Omit & { configProperty?: string; diff --git a/app/client/src/components/formControls/InputTextControl.tsx b/app/client/src/components/formControls/InputTextControl.tsx index d16b6cb74b..af0e0d3b0d 100644 --- a/app/client/src/components/formControls/InputTextControl.tsx +++ b/app/client/src/components/formControls/InputTextControl.tsx @@ -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; - validator?: (value: string) => { isValid: boolean; message: string }; -}) { - const { dataType, disabled, name, placeholder } = props; +const FieldWrapper = styled.div` + width: 35vw; + position: relative; +`; - return ( -
- -
- ); -} +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; @@ -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 { + 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 { } = this.props; return ( - + + {this.state.secretDisplayVisible && ( + + )} + + ); } @@ -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); diff --git a/app/client/src/entities/Datasource/RestAPIForm.ts b/app/client/src/entities/Datasource/RestAPIForm.ts index bc875bb082..ee7cec626d 100644 --- a/app/client/src/entities/Datasource/RestAPIForm.ts +++ b/app/client/src/entities/Datasource/RestAPIForm.ts @@ -91,6 +91,7 @@ export interface Basic { authenticationType: AuthType.basic; username: string; password: string; + secretExists?: Record; } export interface ApiKey { diff --git a/app/client/src/entities/Datasource/index.ts b/app/client/src/entities/Datasource/index.ts index 95bbea170c..b8bce27d78 100644 --- a/app/client/src/entities/Datasource/index.ts +++ b/app/client/src/entities/Datasource/index.ts @@ -23,6 +23,7 @@ export interface DatasourceAuthentication { bearerToken?: string; authenticationStatus?: string; authenticationType?: string; + secretExists?: Record; } export interface DatasourceColumns { diff --git a/app/client/src/pages/Editor/DataSourceEditor/RestAPIDatasourceForm.tsx b/app/client/src/pages/Editor/DataSourceEditor/RestAPIDatasourceForm.tsx index 81f63fe245..a035118e0b 100644 --- a/app/client/src/pages/Editor/DataSourceEditor/RestAPIDatasourceForm.tsx +++ b/app/client/src/pages/Editor/DataSourceEditor/RestAPIDatasourceForm.tsx @@ -452,15 +452,15 @@ class DatasourceRestAPIEditor extends React.Component< return (
- {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, + })} {formData.isSendSessionEnabled && ( - {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, + })} )} @@ -591,24 +591,24 @@ class DatasourceRestAPIEditor extends React.Component< return ( <> - {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, + })} - {this.renderInputTextControlViaFormControl( - "authentication.value", - "Value", - "value", - "TEXT", - true, - false, - )} + {this.renderInputTextControlViaFormControl({ + configProperty: "authentication.value", + label: "Value", + placeholderText: "value", + dataType: "TEXT", + encrypted: true, + isRequired: false, + })} {this.renderDropdownControlViaFormControl( @@ -633,14 +633,14 @@ class DatasourceRestAPIEditor extends React.Component< - {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, + })} )} @@ -650,14 +650,14 @@ class DatasourceRestAPIEditor extends React.Component< renderBearerToken = () => { return ( - {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, + })} ); }; @@ -666,24 +666,25 @@ class DatasourceRestAPIEditor extends React.Component< return ( <> - {this.renderInputTextControlViaFormControl( - "authentication.username", - "Username", - "Username", - "TEXT", - false, - false, - )} + {this.renderInputTextControlViaFormControl({ + configProperty: "authentication.username", + label: "Username", + placeholderText: "Username", + dataType: "TEXT", + encrypted: false, + isRequired: false, + })} - {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", + })} ); @@ -757,60 +758,61 @@ class DatasourceRestAPIEditor extends React.Component< - {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, + })} )} - {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, + })} - {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, + })} - {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", + })} - {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, + })} - {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, + })} - {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, + })} ); @@ -976,14 +978,14 @@ class DatasourceRestAPIEditor extends React.Component< - {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, + })}
@@ -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 (