Skip to content
This repository has been archived by the owner on Oct 20, 2023. It is now read-only.

Improve LoginForm UsernameInput Bugginess #191

Merged
merged 9 commits into from
Sep 21, 2023
25 changes: 19 additions & 6 deletions applications/client/src/components/Dialogs/UserSettingsOverlay.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,36 @@
import { Button } from '@blueprintjs/core';
import type { DialogExProps } from '@redeye/client/components';
import { DialogBodyEx, DialogEx, LoginForm } from '@redeye/client/components';
import { DialogBodyEx, DialogEx, DialogFooterEx } from '@redeye/client/components';
import { useStore } from '@redeye/client/store';
import { Txt } from '@redeye/ui-styles';
import { observer } from 'mobx-react-lite';
import type { FormEventHandler } from 'react';

type UserSettingsOverlayProps = DialogExProps & {
onSubmit?: FormEventHandler<HTMLFormElement>;
};

export const UserSettingsOverlay = observer<UserSettingsOverlayProps>(({ onSubmit, ...props }) => {
export const UserSettingsOverlay = observer<UserSettingsOverlayProps>(({ onSubmit, onClose, ...props }) => {
const store = useStore();
return (
<DialogEx title="User Settings" {...props}>
<DialogEx title="User" onClose={onClose} {...props}>
<DialogBodyEx>
<p>Change Username</p>
<LoginForm submitText="Update" cy-test="update" onSubmit={onSubmit} />
<Button minimal text="Log out" cy-test="logout" onClick={() => store.auth.logOut()} css={{ marginLeft: -7 }} />
<Txt>Logged in as {store.auth.userName}</Txt>
</DialogBodyEx>
<DialogFooterEx
actions={
<>
<Button text="Close" onClick={onClose} />
<Button
intent="warning"
text="Log out"
cy-test="logout"
onClick={() => store.auth.logOut()}
css={{ marginLeft: -7 }}
/>
</>
}
/>
</DialogEx>
);
});
40 changes: 10 additions & 30 deletions applications/client/src/components/Forms/LoginForm.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Button, Callout, Collapse, InputGroup, Intent } from '@blueprintjs/core';
import { Button, Callout, InputGroup, Intent } from '@blueprintjs/core';
import { ArrowRight16, Password16, Warning20 } from '@carbon/icons-react';
import { css } from '@emotion/react';
import { CarbonIcon, UsernameInput } from '@redeye/client/components';
Expand All @@ -10,16 +10,12 @@ import { observer } from 'mobx-react-lite';
import type { ComponentProps, FormEvent } from 'react';
import { createState } from '../mobx-create-state';

type LoginFormProps = ComponentProps<'form'> & {
submitText?: string;
};
type LoginFormProps = ComponentProps<'form'>;

const isDevelop = import.meta.env.DEV;

export const LoginForm = observer<LoginFormProps>(({ onSubmit, submitText = 'Login', ...props }) => {
export const LoginForm = observer<LoginFormProps>(({ ...props }) => {
const store = useStore();
const state = createState({
username: isDevelop ? store.auth.userName || 'dev' : store.auth.userName || '',
username: store.auth.userName || '',
password: '',
passwordFocus: false,
loading: false,
Expand All @@ -30,7 +26,6 @@ export const LoginForm = observer<LoginFormProps>(({ onSubmit, submitText = 'Log
if (store.appMeta.blueTeam) {
store.auth.setUser(this.username);
store.router.updateRoute({ path: routes[Views.CAMPAIGNS_LIST], params: { id: 'all' } });
if (onSubmit) onSubmit(event);
} else {
// Make login call
const formData = new FormData();
Expand Down Expand Up @@ -58,7 +53,6 @@ export const LoginForm = observer<LoginFormProps>(({ onSubmit, submitText = 'Log
}
store.auth.setUser(this.username);
store.router.updateRoute({ path: routes[Views.CAMPAIGNS_LIST], params: { id: 'all' } });
if (onSubmit) onSubmit(event);
}
} catch (e) {
this.loading = false;
Expand Down Expand Up @@ -101,26 +95,26 @@ export const LoginForm = observer<LoginFormProps>(({ onSubmit, submitText = 'Log
username={state.username}
password={state.password}
refetch={refetch}
disableCreateUser={!data}
softDisable={!data}
users={data?.globalOperators}
updateUser={(userName) => state.update('username', userName)}
onUsernameUpdate={(username) => state.update('username', username)}
css={otherSpacingLooseStyle}
/>
<Collapse isOpen={!!state.errorMessage}>
{!!state.errorMessage && (
<Callout
css={otherSpacingLooseStyle}
intent={Intent.DANGER}
icon={<CarbonIcon icon={Warning20} />}
children={state.errorMessage}
/>
</Collapse>
)}
<Button
cy-test="login-btn"
text={submitText}
text="Login"
loading={state.loading}
intent="primary"
css={otherSpacingLooseStyle}
disabled={state.username.length < 1}
disabled={state.username.length < 1 || (state.password.length < 1 && !store.appMeta.blueTeam)}
type="submit"
rightIcon={<CarbonIcon icon={ArrowRight16} />}
large
Expand All @@ -135,17 +129,3 @@ const inputSpacingTightStyle = css`
const otherSpacingLooseStyle = css`
margin-bottom: 1rem;
`;
// const removeAutofillStyle = css`
/* // don't think this is a good idea
box-shadow: rgba(255, 255, 255, 0.4) 0px -1px 0px 0px inset !important;
background: rgba(255, 255, 255, 0.06);
input:not(:-webkit-autofill) {
background: transparent !important;
}
input:-webkit-autofill {
-webkit-text-fill-color: rgba(255, 255, 255, 0.96);
-webkit-background-image: none !important;
-webkit-background-clip: text;
}
*/
// `;
164 changes: 88 additions & 76 deletions applications/client/src/components/Forms/UsernameInput.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Classes, MenuItem } from '@blueprintjs/core';
import type { ItemPredicate, SuggestProps } from '@blueprintjs/select';
import { Suggest, getCreateNewItem } from '@blueprintjs/select';
import { Add16, User16 } from '@carbon/icons-react';
import { ClassNames, css } from '@emotion/react';
import { User16, UserFollow16 } from '@carbon/icons-react';
import { css } from '@emotion/react';
import { CarbonIcon, createState, escapeRegExpChars } from '@redeye/client/components';
import type { GlobalOperatorModel } from '@redeye/client/store';
import { useStore } from '@redeye/client/store';
Expand All @@ -16,18 +16,18 @@ type UsernameInputProps = Omit<
> & {
username: string;
password: string;
disableCreateUser: boolean;
softDisable: boolean;
refetch: () => any;
users?: GlobalOperatorModel[];
updateUser: (userName) => void;
onUsernameUpdate: (username) => void;
};

export const UsernameInput = observer<UsernameInputProps>(
({ username, password, disableCreateUser, refetch, users = [], updateUser, ...props }) => {
({ username, password, softDisable, refetch, users = [], onUsernameUpdate, ...props }) => {
const store = useStore();

const state = createState({
active: null as null | GlobalOperatorModel,
activeItem: null as null | GlobalOperatorModel,
query: username,
});

Expand All @@ -41,82 +41,94 @@ export const UsernameInput = observer<UsernameInputProps>(
onSuccess(op) {
if (op?.createGlobalOperator) {
refetch();
updateUser(state.query);
onUsernameUpdate(state.query);
}
},
}
);

return (
<ClassNames>
{({ css: classCss }) => (
// for the popoverProps.className
<Suggest
cy-test="username"
openOnKeyDown
query={state.query}
createNewItemFromQuery={(query) => ({ name: query, id: query } as any)}
selectedItem={
store.graphqlStore.globalOperators.get(state.query) ||
({ name: state.query, id: state.query } as GlobalOperatorModel)
}
itemPredicate={filterUsers}
activeItem={state.active || getCreateNewItem()}
onItemSelect={(item) => {
state.update('query', item?.name);
updateUser(item.name);
}}
onActiveItemChange={(active) => state.update('active', active)}
onQueryChange={(query) => state.update('query', query)}
items={users || []}
inputValueRenderer={(item) => item.name as string}
css={menuParentStyle}
fill
popoverProps={
{
minimal: true,
popoverClassName: classCss(menuParentStyle),
} as any
}
inputProps={{
// value: state.query, // not needed in bp5
onBlur: () => updateUser(state.query),
type: 'text',
name: 'username',
autoComplete: 'username',
placeholder: 'user',
leftIcon: <CarbonIcon icon={User16} />,
large: true,
}}
itemRenderer={(user, { handleClick, modifiers }) => {
if (!modifiers.matchesPredicate) return null;
return (
<MenuItem
text={highlightText(user.name as string, state.query)}
active={modifiers.active}
disabled={modifiers.disabled}
// label={user.campaign} // TODO: add campaign the user comes from
key={user.name}
onClick={handleClick}
/>
);
}}
createNewItemRenderer={(_, isActive: boolean) => (
<MenuItem
icon={<CarbonIcon icon={Add16} />}
text="New User"
disabled={disableCreateUser || store.graphqlStore.globalOperators.has(state.query)}
label={state.query}
active={isActive}
onClick={() => addUser()}
shouldDismissPopover={false}
css={newUserStyle}
/>
)}
{...props}
/>
)}
</ClassNames>
<Suggest
cy-test="username"
query={state.query}
createNewItemFromQuery={(query) => ({ name: query, id: query } as any)}
selectedItem={
store.graphqlStore.globalOperators.get(state.query) ||
({ name: state.query, id: state.query } as GlobalOperatorModel)
}
itemPredicate={filterUsers}
activeItem={state.activeItem || getCreateNewItem()}
onItemSelect={(item) => {
state.update('query', item?.name);
onUsernameUpdate(item.name);
}}
onActiveItemChange={(activeItem) => {
state.update('activeItem', activeItem);
// if there is a plausible matching username, use it.
// otherwise, used the typed text
onUsernameUpdate(activeItem ? activeItem.name : state.query);
}}
onQueryChange={(query) => {
state.update('query', query);
}}
items={users || []}
inputValueRenderer={(item) => item.name as string}
css={menuParentStyle}
fill
popoverProps={{
minimal: true,
matchTargetWidth: true,
}}
inputProps={{
type: 'text',
name: 'username',
autoComplete: 'username',
placeholder: 'user',
leftIcon: <CarbonIcon icon={User16} />,
large: true,
}}
itemRenderer={(user, { handleClick, modifiers }) => {
if (!modifiers.matchesPredicate) return null;
return (
<MenuItem
text={highlightText(user.name as string, state.query)}
active={modifiers.active}
disabled={modifiers.disabled}
// label={user.campaign} // TODO: add campaign the user comes from
key={user.name}
onClick={handleClick}
/>
);
}}
createNewItemRenderer={
store.appMeta.blueTeam
? undefined
: (_, isActive: boolean) =>
softDisable ? (
<div css={{ padding: '4px 12px' }}>
<Txt small bold block css={password && { color: CoreTokens.TextIntentDanger }}>
{password ? 'Invalid Server Password' : 'No Server Password'}
</Txt>
<Txt small italic muted block>
Enter a valid password to see user options
</Txt>
</div>
) : (
<MenuItem
icon={<CarbonIcon icon={UserFollow16} />}
text={state.query}
disabled={store.graphqlStore.globalOperators.has(state.query)}
label="Add new user"
active={isActive}
onClick={() => addUser()}
shouldDismissPopover={false}
intent="primary"
css={newUserStyle}
/>
)
}
{...props}
/>
);
}
);
Expand Down
Loading