From 33d26258c30644b7d11489e7b47c1ae9cdaf996e Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Tue, 21 Mar 2023 19:45:08 -0400 Subject: [PATCH] feat(cred): credential test table Signed-off-by: Thuan Vo --- src/app/AppLayout/CredentialAuthForm.tsx | 58 ++- .../Credentials/CreateCredentialModal.tsx | 180 ++++++--- .../Credentials/CredentialTestTable.tsx | 347 ++++++++++++++++++ src/app/SecurityPanel/Credentials/utils.tsx | 70 ++++ src/app/Shared/Services/Api.service.tsx | 39 +- src/app/app.css | 6 +- src/app/utils/utils.ts | 17 + 7 files changed, 648 insertions(+), 69 deletions(-) create mode 100644 src/app/SecurityPanel/Credentials/CredentialTestTable.tsx create mode 100644 src/app/SecurityPanel/Credentials/utils.tsx diff --git a/src/app/AppLayout/CredentialAuthForm.tsx b/src/app/AppLayout/CredentialAuthForm.tsx index 99262694e9..32245c2af5 100644 --- a/src/app/AppLayout/CredentialAuthForm.tsx +++ b/src/app/AppLayout/CredentialAuthForm.tsx @@ -39,15 +39,31 @@ import { LoadingPropsType } from '@app/Shared/ProgressIndicator'; import { ActionGroup, Button, Form, FormGroup, TextInput } from '@patternfly/react-core'; import * as React from 'react'; +export interface AuthCredential { + username: string; + password: string; +} + export interface CredentialAuthFormProps { onDismiss: () => void; onSave: (username: string, password: string) => void; focus?: boolean; loading?: boolean; + isDisabled?: boolean; children?: React.ReactNode; + onCredentialChange?: (credential: AuthCredential) => void; } -export const CredentialAuthForm: React.FC = ({ onDismiss, onSave, ...props }) => { +export const CredentialAuthForm: React.FC = ({ + onDismiss, + onSave, + onCredentialChange, + loading, + isDisabled, + focus, + children, + ...props +}) => { const [username, setUsername] = React.useState(''); const [password, setPassword] = React.useState(''); @@ -73,35 +89,49 @@ export const CredentialAuthForm: React.FC = ({ onDismis () => ({ spinnerAriaValueText: 'Saving', - spinnerAriaLabel: 'saving-jmx-credentials', - isLoading: props.loading, + spinnerAriaLabel: 'saving-credentials', + isLoading: loading, } as LoadingPropsType), - [props.loading] + [loading] ); return ( -
- {props.children} + + {children} { + setUsername(v); + onCredentialChange && + onCredentialChange({ + username: v, + password: password, + }); + }} onKeyUp={handleKeyUp} - autoFocus={props.focus} + autoFocus={focus} /> { + setPassword(v); + onCredentialChange && + onCredentialChange({ + username: username, + password: v, + }); + }} onKeyUp={handleKeyUp} /> @@ -110,11 +140,11 @@ export const CredentialAuthForm: React.FC = ({ onDismis variant="primary" onClick={handleSave} {...saveButtonLoadingProps} - isDisabled={props.loading || username === '' || password === ''} + isDisabled={isDisabled || loading || username === '' || password === ''} > - {props.loading ? 'Saving' : 'Save'} + {loading ? 'Saving' : 'Save'} - diff --git a/src/app/SecurityPanel/Credentials/CreateCredentialModal.tsx b/src/app/SecurityPanel/Credentials/CreateCredentialModal.tsx index 30a1ca8df4..12b0a22047 100644 --- a/src/app/SecurityPanel/Credentials/CreateCredentialModal.tsx +++ b/src/app/SecurityPanel/Credentials/CreateCredentialModal.tsx @@ -35,14 +35,14 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -import { CredentialAuthForm } from '@app/AppLayout/CredentialAuthForm'; +import { AuthCredential, CredentialAuthForm } from '@app/AppLayout/CredentialAuthForm'; import { MatchExpressionHint } from '@app/Shared/MatchExpression/MatchExpressionHint'; import { MatchExpressionVisualizer } from '@app/Shared/MatchExpression/MatchExpressionVisualizer'; import { ServiceContext } from '@app/Shared/Services/Services'; import { Target } from '@app/Shared/Services/Target.service'; import { SearchExprService, SearchExprServiceContext } from '@app/Topology/Shared/utils'; import { useSubscriptions } from '@app/utils/useSubscriptions'; -import { evaluateTargetWithExpr, portalRoot } from '@app/utils/utils'; +import { evaluateTargetWithExpr, portalRoot, StreamOf } from '@app/utils/utils'; import { Button, Card, @@ -53,11 +53,17 @@ import { Modal, ModalVariant, Popover, + Tab, + Tabs, + TabTitleIcon, + TabTitleText, TextArea, ValidatedOptions, } from '@patternfly/react-core'; -import { HelpIcon } from '@patternfly/react-icons'; +import { FlaskIcon, HelpIcon, TopologyIcon } from '@patternfly/react-icons'; import * as React from 'react'; +import { CredentialTestTable } from './CredentialTestTable'; +import { CredentialContext, TestAllContext, useAuthCredential } from './utils'; export interface CreateCredentialModalProps { visible: boolean; @@ -72,51 +78,61 @@ export const CreateCredentialModal: React.FunctionComponent { const matchExpreRef = React.useRef(new SearchExprService()); + const loadingRef = React.useRef(new StreamOf(false)); + const testAllRef = React.useRef(new StreamOf(false)); + const credentialRef = React.useRef(new StreamOf({ username: '', password: '' })); + const addSubscription = useSubscriptions(); + const [inProgress, setInProgress] = React.useState(false); - const alertOptions = React.useMemo(() => ({ hideActions: true }), []); + React.useEffect(() => { + addSubscription(loadingRef.current.get().subscribe(setInProgress)); + }, [addSubscription, loadingRef, setInProgress]); return ( - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + ); }; -interface AuthFormProps extends CreateCredentialModalProps { +interface AuthFormProps extends Omit { progressChange?: (inProgress: boolean) => void; } @@ -126,29 +142,26 @@ export const AuthForm: React.FC = ({ onDismiss, onPropsSave, prog const matchExprService = React.useContext(SearchExprServiceContext); const [matchExpression, setMatchExpression] = React.useState(''); const [matchExpressionValid, setMatchExpressionValid] = React.useState(ValidatedOptions.default); - const [loading, setLoading] = React.useState(false); + const [_, setCredential] = useAuthCredential(true); + const [saving, setSaving] = React.useState(false); const [targets, setTargets] = React.useState([]); const onSave = React.useCallback( (username: string, password: string) => { - setLoading(true); + setSaving(true); addSubscription( context.api.postCredentials(matchExpression, username, password).subscribe((ok) => { - setLoading(false); + setSaving(false); if (ok) { onPropsSave(); } }) ); }, - [addSubscription, onPropsSave, context.api, matchExpression, setLoading] + [addSubscription, onPropsSave, context.api, matchExpression, setSaving] ); - React.useEffect(() => { - progressChange && progressChange(loading); - }, [loading, progressChange]); - React.useEffect(() => { addSubscription(context.targets.targets().subscribe(setTargets)); }, [addSubscription, context.targets, setTargets]); @@ -157,7 +170,13 @@ export const AuthForm: React.FC = ({ onDismiss, onPropsSave, prog let validation: ValidatedOptions = ValidatedOptions.default; if (matchExpression !== '' && targets.length > 0) { try { - const atLeastOne = targets.some((t) => evaluateTargetWithExpr(t, matchExpression)); + const atLeastOne = targets.some((t) => { + const res = evaluateTargetWithExpr(t, matchExpression); + if (typeof res === 'boolean') { + return res; + } + throw new Error('Invalid match expression'); + }); validation = atLeastOne ? ValidatedOptions.success : ValidatedOptions.warning; } catch (err) { validation = ValidatedOptions.error; @@ -166,8 +185,20 @@ export const AuthForm: React.FC = ({ onDismiss, onPropsSave, prog setMatchExpressionValid(validation); }, [matchExpression, targets, setMatchExpressionValid]); + React.useEffect(() => { + progressChange && progressChange(saving); + }, [saving, progressChange]); + return ( - + = ({ onDismiss, onPropsSave, prog >