Skip to content

Commit

Permalink
feat(cred): credential test table
Browse files Browse the repository at this point in the history
Signed-off-by: Thuan Vo <thvo@redhat.com>
  • Loading branch information
Thuan Vo committed Mar 23, 2023
1 parent 547a37d commit 33d2625
Show file tree
Hide file tree
Showing 7 changed files with 648 additions and 69 deletions.
58 changes: 44 additions & 14 deletions src/app/AppLayout/CredentialAuthForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<CredentialAuthFormProps> = ({ onDismiss, onSave, ...props }) => {
export const CredentialAuthForm: React.FC<CredentialAuthFormProps> = ({
onDismiss,
onSave,
onCredentialChange,
loading,
isDisabled,
focus,
children,
...props
}) => {
const [username, setUsername] = React.useState('');
const [password, setPassword] = React.useState('');

Expand All @@ -73,35 +89,49 @@ export const CredentialAuthForm: React.FC<CredentialAuthFormProps> = ({ onDismis
() =>
({
spinnerAriaValueText: 'Saving',
spinnerAriaLabel: 'saving-jmx-credentials',
isLoading: props.loading,
spinnerAriaLabel: 'saving-credentials',
isLoading: loading,
} as LoadingPropsType),
[props.loading]
[loading]
);

return (
<Form>
{props.children}
<Form {...props}>
{children}
<FormGroup isRequired label="Username" fieldId="username">
<TextInput
value={username}
isDisabled={props.loading}
isDisabled={isDisabled || loading}
isRequired
type="text"
id="username"
onChange={setUsername}
onChange={(v) => {
setUsername(v);
onCredentialChange &&
onCredentialChange({
username: v,
password: password,
});
}}
onKeyUp={handleKeyUp}
autoFocus={props.focus}
autoFocus={focus}
/>
</FormGroup>
<FormGroup isRequired label="Password" fieldId="password">
<TextInput
value={password}
isDisabled={props.loading}
isDisabled={isDisabled || loading}
isRequired
type="password"
id="password"
onChange={setPassword}
onChange={(v) => {
setPassword(v);
onCredentialChange &&
onCredentialChange({
username: username,
password: v,
});
}}
onKeyUp={handleKeyUp}
/>
</FormGroup>
Expand All @@ -110,11 +140,11 @@ export const CredentialAuthForm: React.FC<CredentialAuthFormProps> = ({ onDismis
variant="primary"
onClick={handleSave}
{...saveButtonLoadingProps}
isDisabled={props.loading || username === '' || password === ''}
isDisabled={isDisabled || loading || username === '' || password === ''}
>
{props.loading ? 'Saving' : 'Save'}
{loading ? 'Saving' : 'Save'}
</Button>
<Button variant="secondary" onClick={handleDismiss} isDisabled={props.loading}>
<Button variant="secondary" onClick={handleDismiss} isDisabled={isDisabled || loading}>
Cancel
</Button>
</ActionGroup>
Expand Down
180 changes: 128 additions & 52 deletions src/app/SecurityPanel/Credentials/CreateCredentialModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -72,51 +78,61 @@ export const CreateCredentialModal: React.FunctionComponent<CreateCredentialModa
...props
}) => {
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<AuthCredential>({ 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 (
<SearchExprServiceContext.Provider value={matchExpreRef.current}>
<Modal
appendTo={portalRoot}
isOpen={visible}
tabIndex={0} // enable keyboard-accessible scrolling
variant={ModalVariant.large}
showClose={!inProgress}
className="add-credential-modal"
onClose={onDismiss}
title="Store Credentials"
description="Create stored credentials for target JVMs. Cryostat will use these credentials to connect to Cryostat agents or target JVMs over JMX (if required)."
>
<Grid hasGutter style={{ height: '100%' }}>
<GridItem xl={4}>
<Card isFullHeight isFlat>
<CardBody className="overflow-auto">
<AuthForm
visible={visible}
onDismiss={onDismiss}
onPropsSave={onPropsSave}
progressChange={setInProgress}
{...props}
/>
</CardBody>
</Card>
</GridItem>
<GridItem xl={8}>
<Card isFullHeight isFlat>
<CardBody className="overflow-auto">
<MatchExpressionVisualizer alertOptions={alertOptions} />
</CardBody>
</Card>
</GridItem>
</Grid>
</Modal>
</SearchExprServiceContext.Provider>
<Modal
appendTo={portalRoot}
isOpen={visible}
tabIndex={0} // enable keyboard-accessible scrolling
variant={ModalVariant.large}
showClose={!inProgress}
className="add-credential-modal"
onClose={onDismiss}
title="Store Credentials"
description="Create stored credentials for target JVMs. Cryostat will use these credentials to connect to Cryostat agents or target JVMs over JMX (if required)."
>
<SearchExprServiceContext.Provider value={matchExpreRef.current}>
<CredentialContext.Provider value={credentialRef.current}>
<TestAllContext.Provider value={testAllRef.current}>
<Grid hasGutter style={{ height: '100%' }}>
<GridItem xl={4}>
<Card isFullHeight isFlat>
<CardBody className="overflow-auto">
<AuthForm
{...props}
onDismiss={onDismiss}
onPropsSave={onPropsSave}
progressChange={setInProgress}
/>
</CardBody>
</Card>
</GridItem>
<GridItem xl={8}>
<Card isFullHeight isFlat>
<CardBody className="overflow-auto">
<FormHelper />
</CardBody>
</Card>
</GridItem>
</Grid>
</TestAllContext.Provider>
</CredentialContext.Provider>
</SearchExprServiceContext.Provider>
</Modal>
);
};

interface AuthFormProps extends CreateCredentialModalProps {
interface AuthFormProps extends Omit<CreateCredentialModalProps, 'visible'> {
progressChange?: (inProgress: boolean) => void;
}

Expand All @@ -126,29 +142,26 @@ export const AuthForm: React.FC<AuthFormProps> = ({ 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<Target[]>([]);

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]);
Expand All @@ -157,7 +170,13 @@ export const AuthForm: React.FC<AuthFormProps> = ({ 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;
Expand All @@ -166,8 +185,20 @@ export const AuthForm: React.FC<AuthFormProps> = ({ onDismiss, onPropsSave, prog
setMatchExpressionValid(validation);
}, [matchExpression, targets, setMatchExpressionValid]);

React.useEffect(() => {
progressChange && progressChange(saving);
}, [saving, progressChange]);

return (
<CredentialAuthForm {...props} onSave={onSave} onDismiss={onDismiss} focus={false} loading={loading}>
<CredentialAuthForm
{...props}
onSave={onSave}
onDismiss={onDismiss}
focus={false}
loading={saving}
isDisabled={false}
onCredentialChange={setCredential}
>
<FormGroup
label="Match Expression"
labelIcon={
Expand Down Expand Up @@ -207,7 +238,7 @@ export const AuthForm: React.FC<AuthFormProps> = ({ onDismiss, onPropsSave, prog
>
<TextArea
value={matchExpression}
isDisabled={loading}
isDisabled={false}
isRequired
type="text"
id="rule-matchexpr"
Expand All @@ -225,3 +256,48 @@ export const AuthForm: React.FC<AuthFormProps> = ({ onDismiss, onPropsSave, prog
</CredentialAuthForm>
);
};

type _SupportedTab = 'visualizer' | 'test';

export const FormHelper: React.FC = ({ ...props }) => {
const alertOptions = React.useMemo(() => ({ hideActions: true }), []);
const [activeTab, setActiveTab] = React.useState<_SupportedTab>('visualizer');

const handleTabChange = React.useCallback(
(_: React.MouseEvent, key: string | number) => setActiveTab(`${key}` as _SupportedTab),
[setActiveTab]
);

return (
<Tabs {...props} activeKey={activeTab} onSelect={handleTabChange}>
<Tab
eventKey={'visualizer'}
title={
<>
<TabTitleIcon>
<TopologyIcon />
</TabTitleIcon>
<TabTitleText>Visualizer</TabTitleText>
</>
}
>
<div style={{ marginTop: '1em', height: '100%' }}>
<MatchExpressionVisualizer alertOptions={alertOptions} />
</div>
</Tab>
<Tab
eventKey={'test'}
title={
<>
<TabTitleIcon>
<FlaskIcon />
</TabTitleIcon>
<TabTitleText>Test</TabTitleText>
</>
}
>
<CredentialTestTable />
</Tab>
</Tabs>
);
};
Loading

0 comments on commit 33d2625

Please sign in to comment.