Skip to content

Commit ae96f28

Browse files
authored
Merge pull request #3050 from alexcottner/implement-spinners-cleaner
Add spinners and disable forms while data is being loaded. Resolving several issues around users interacting with data while waiting on server response.
2 parents 52da91b + caef5fa commit ae96f28

13 files changed

+504
-102
lines changed

css/styles.css

+56
Original file line numberDiff line numberDiff line change
@@ -559,14 +559,70 @@ i.fa:before {
559559
font-size: 12pt;
560560
}
561561

562+
.formWrapper {
563+
position: relative;
564+
}
565+
566+
.formWrapper > .spinner {
567+
position: absolute;
568+
left: -1.25em;
569+
right: -1.25em;
570+
top: -1.5em;
571+
bottom: -2.5em;
572+
background-color: rgba(0, 0, 0, 0.2);
573+
z-index: 1;
574+
display: flex;
575+
justify-content: center;
576+
align-items: center;
577+
width: auto;
578+
margin: 0;
579+
border-radius: 3px;
580+
}
581+
582+
.interactive > .spinner {
583+
position: absolute;
584+
left: -1.25em;
585+
right: -1.25em;
586+
top: -.5em;
587+
bottom: 0;
588+
background-color: rgba(0, 0, 0, 0.2);
589+
z-index: 1;
590+
display: flex;
591+
justify-content: center;
592+
align-items: center;
593+
width: auto;
594+
margin: 0;
595+
border-radius: 3px;
596+
}
597+
562598
.modal {
563599
background-color: rgba(0, 0, 0, 0.3);
564600
}
565601

602+
.modal-dialog > .spinner {
603+
position: absolute;
604+
left: 0;
605+
right: 0;
606+
top: 0;
607+
bottom: 0;
608+
background-color: rgba(0, 0, 0, 0.2);
609+
z-index: 1;
610+
display: flex;
611+
justify-content: center;
612+
align-items: center;
613+
width: auto;
614+
margin: 0;
615+
border-radius: 3px;
616+
}
617+
566618
.informative {
567619
filter: grayscale(0.8);
568620
}
569621

622+
.interactive {
623+
position: relative;
624+
}
625+
570626
/*Form Wizard*/
571627
.bs-wizard {margin-top: 40px;}
572628
.progress-steps .row.bs-wizard { border-bottom: 0; }

src/components/AuthForm.tsx

+37-25
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,20 @@ function extendUiSchemaWithHistory(
256256
};
257257
}
258258

259+
function getSupportedAuthMethods(session: SessionState): string[] {
260+
const {
261+
serverInfo: { capabilities },
262+
} = session;
263+
const { openid: { providers } = { providers: [] } } = capabilities;
264+
// Check which of our known auth implementations are supported by the server.
265+
const supportedAuthMethods = KNOWN_AUTH_METHODS.filter(
266+
a => a in capabilities
267+
);
268+
// Add an auth method for each single openid provider supported by the server.
269+
const openIdMethods = providers.map(provider => `openid-${provider.name}`);
270+
return [ANONYMOUS_AUTH].concat(supportedAuthMethods).concat(openIdMethods);
271+
}
272+
259273
type AuthFormProps = {
260274
session: SessionState;
261275
servers: ServerEntry[];
@@ -281,35 +295,43 @@ export default function AuthForm({
281295
const { schema: currentSchema, uiSchema: curentUiSchema } =
282296
authSchemas(authType);
283297

298+
const [showSpinner, setshowSpinner] = useState(false);
284299
const [schema, setSchema] = useState(currentSchema);
285300
const [uiSchema, setUiSchema] = useState(curentUiSchema);
286301
const [formData, setFormData] = useState({
287302
authType,
288303
server: getServerByPriority(servers),
289304
});
290305

291-
const getSupportedAuthMethods = (): string[] => {
292-
const {
293-
serverInfo: { capabilities },
294-
} = session;
295-
const { openid: { providers } = { providers: [] } } = capabilities;
296-
// Check which of our known auth implementations are supported by the server.
297-
const supportedAuthMethods = KNOWN_AUTH_METHODS.filter(
298-
a => a in capabilities
299-
);
300-
// Add an auth method for each single openid provider supported by the server.
301-
const openIdMethods = providers.map(provider => `openid-${provider.name}`);
302-
return [ANONYMOUS_AUTH].concat(supportedAuthMethods).concat(openIdMethods);
306+
const serverChangeCallback = () => {
307+
serverChange();
303308
};
304309

310+
const serverInfoCallback = auth => {
311+
getServerInfo(auth);
312+
setshowSpinner(false);
313+
};
314+
315+
const authMethods = getSupportedAuthMethods(session);
316+
const singleAuthMethod = authMethods.length === 1;
317+
const finalSchema = extendSchemaWithHistory(schema, servers, authMethods);
318+
const finalUiSchema = extendUiSchemaWithHistory(
319+
uiSchema,
320+
servers,
321+
clearServers,
322+
serverInfoCallback,
323+
serverChangeCallback,
324+
singleAuthMethod
325+
);
326+
305327
const onChange = ({ formData: updatedData }: RJSFSchema) => {
306328
if (formData.server !== updatedData.server) {
329+
setshowSpinner(true);
307330
const newServer = servers.find(x => x.server === updatedData.server);
308331
updatedData.authType = newServer?.authType || ANONYMOUS_AUTH;
309332
}
310333
const { authType } = updatedData;
311-
const { uiSchema } = authSchemas(authType);
312-
const { schema } = authSchemas(authType);
334+
const { schema, uiSchema } = authSchemas(authType);
313335
const omitCredentials =
314336
[ANONYMOUS_AUTH, "fxa", "portier"].includes(authType) ||
315337
authType.startsWith("openid-");
@@ -355,17 +377,6 @@ export default function AuthForm({
355377
}
356378
};
357379

358-
const authMethods = getSupportedAuthMethods();
359-
const singleAuthMethod = authMethods.length === 1;
360-
const finalSchema = extendSchemaWithHistory(schema, servers, authMethods);
361-
const finalUiSchema = extendUiSchemaWithHistory(
362-
uiSchema,
363-
servers,
364-
clearServers,
365-
getServerInfo,
366-
serverChange,
367-
singleAuthMethod
368-
);
369380
return (
370381
<div className="card">
371382
<div className="card-body">
@@ -375,6 +386,7 @@ export default function AuthForm({
375386
formData={formData}
376387
onChange={onChange}
377388
onSubmit={onSubmit}
389+
showSpinner={showSpinner}
378390
>
379391
<button type="submit" className="btn btn-info">
380392
{"Sign in using "}

src/components/BaseForm.tsx

+26-10
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,42 @@
1-
import React from "react";
1+
import React, { useState } from "react";
22
import { withTheme, FormProps } from "@rjsf/core";
33
import { Theme as Bootstrap4Theme } from "@rjsf/bootstrap-4";
44
import validator from "@rjsf/validator-ajv8";
5+
import { RJSFSchema } from "@rjsf/utils";
56

67
import TagsField from "./TagsField";
8+
import Spinner from "./Spinner";
79

810
const adminFields = { tags: TagsField };
911

1012
const FormWithTheme = withTheme(Bootstrap4Theme);
1113

12-
export type BaseFormProps = Omit<FormProps, "validator">;
14+
export type BaseFormProps = Omit<FormProps, "validator"> & {
15+
showSpinner?: boolean;
16+
onSubmit: (data: RJSFSchema) => void;
17+
};
1318

1419
export default function BaseForm(props: BaseFormProps) {
15-
const { className, ...restProps } = props;
20+
const [isSubmitting, setIsSubmitting] = useState(false);
21+
const { className, disabled, showSpinner, onSubmit, ...restProps } = props;
22+
23+
const handleOnSubmit = form => {
24+
setIsSubmitting(true);
25+
onSubmit(form);
26+
};
1627

1728
return (
18-
<FormWithTheme
19-
{...restProps}
20-
className={`rjsf ${className ? className : ""}`}
21-
validator={validator}
22-
// @ts-ignore
23-
fields={adminFields}
24-
/>
29+
<div className="formWrapper">
30+
<FormWithTheme
31+
{...restProps}
32+
className={`rjsf ${className ? className : ""}`}
33+
validator={validator}
34+
onSubmit={handleOnSubmit}
35+
// @ts-ignore
36+
fields={adminFields}
37+
disabled={disabled || showSpinner || isSubmitting}
38+
/>
39+
{(isSubmitting || showSpinner) && <Spinner />}
40+
</div>
2541
);
2642
}

src/components/ServerHistory.tsx

+13-6
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@ const anonymousAuthData = server => ({
1414
});
1515

1616
type ServerHistoryProps = {
17+
disabled?: boolean;
1718
id: string;
1819
value: string;
1920
placeholder: string;
2021
options: any;
2122
onChange: (s: string) => void;
2223
};
2324

25+
const debounceMillis = 400;
26+
2427
export default function ServerHistory(props: ServerHistoryProps) {
2528
const [value, setValue] = useState(props.value);
2629

@@ -46,12 +49,12 @@ export default function ServerHistory(props: ServerHistoryProps) {
4649
const onServerChange = useCallback(
4750
event => {
4851
const server = event.target.value;
49-
props.onChange(server);
52+
setValue(server);
53+
5054
// Do not try to fetch server info if the field value is invalid.
5155
if (server && event.target.validity && event.target.validity.valid) {
5256
debouncedFetchServerInfo(server);
5357
}
54-
setValue(server);
5558
},
5659
[props]
5760
);
@@ -60,17 +63,19 @@ export default function ServerHistory(props: ServerHistoryProps) {
6063
server => {
6164
// Server changed, request its capabilities to check what auth methods it supports.
6265
const { getServerInfo, serverChange } = props.options;
66+
props.onChange(server);
6367
serverChange();
6468
getServerInfo(anonymousAuthData(server));
6569
},
6670
[props]
6771
);
6872

69-
const debouncedFetchServerInfo = useCallback(debounce(fetchServerInfo, 500), [
70-
fetchServerInfo,
71-
]);
73+
const debouncedFetchServerInfo = useCallback(
74+
debounce(fetchServerInfo, debounceMillis),
75+
[fetchServerInfo]
76+
);
7277

73-
const { id, placeholder, options } = props;
78+
const { id, placeholder, options, disabled = false } = props;
7479
const { servers, pattern } = options;
7580

7681
return (
@@ -82,11 +87,13 @@ export default function ServerHistory(props: ServerHistoryProps) {
8287
pattern={pattern}
8388
value={value}
8489
onChange={onServerChange}
90+
disabled={disabled}
8591
/>
8692
<DropdownButton
8793
as={InputGroup.Append}
8894
variant="outline-secondary"
8995
title="Servers"
96+
disabled={disabled}
9097
>
9198
{servers.length === 0 ? (
9299
<Dropdown.Item>

src/components/Spinner.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React from "react";
22

33
export default function Spinner() {
44
return (
5-
<div className="spinner">
5+
<div className="spinner" data-testid="spinner">
66
<div className="bounce1" />
77
<div className="bounce2" />
88
<div className="bounce3" />

src/components/collection/JSONCollectionForm.tsx

+21-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from "react";
1+
import React, { useState } from "react";
22
import { withTheme } from "@rjsf/core";
33
import { RJSFSchema, UiSchema } from "@rjsf/utils";
44
import { Theme as Bootstrap4Theme } from "@rjsf/bootstrap-4";
@@ -7,6 +7,7 @@ import validator from "@rjsf/validator-ajv8";
77
import JSONEditor from "../JSONEditor";
88
import { omit } from "../../utils";
99
import type { CollectionData } from "../../types";
10+
import Spinner from "../Spinner";
1011

1112
const FormWithTheme = withTheme(Bootstrap4Theme);
1213

@@ -37,6 +38,7 @@ const uiSchema: UiSchema = {
3738
type Props = {
3839
children?: React.ReactNode;
3940
cid?: string | null;
41+
disabled?: boolean;
4042
formData: CollectionData;
4143
onSubmit: (data: { formData: CollectionData }) => void;
4244
};
@@ -46,12 +48,16 @@ export default function JSONCollectionForm({
4648
cid,
4749
formData,
4850
onSubmit,
51+
disabled,
4952
}: Props) {
53+
const [isSubmitting, setIsSubmitting] = useState(false);
54+
5055
const handleSubmit = ({
5156
formData: formInput,
5257
}: {
5358
formData: { id: string; data: string };
5459
}) => {
60+
setIsSubmitting(true);
5561
const collectionData = { ...JSON.parse(formInput.data), id: formInput.id };
5662
onSubmit({ formData: collectionData });
5763
};
@@ -76,15 +82,19 @@ export default function JSONCollectionForm({
7682
};
7783

7884
return (
79-
<FormWithTheme
80-
schema={schema}
81-
uiSchema={_uiSchema}
82-
formData={formDataSerialized}
83-
validator={validator}
84-
// @ts-ignore
85-
onSubmit={handleSubmit}
86-
>
87-
{children}
88-
</FormWithTheme>
85+
<div className="formWrapper">
86+
<FormWithTheme
87+
schema={schema}
88+
uiSchema={_uiSchema}
89+
formData={formDataSerialized}
90+
validator={validator}
91+
// @ts-ignore
92+
onSubmit={handleSubmit}
93+
disabled={disabled || isSubmitting}
94+
>
95+
{children}
96+
</FormWithTheme>
97+
{isSubmitting && <Spinner />}
98+
</div>
8999
);
90100
}

0 commit comments

Comments
 (0)