Added SpinWhileTrue, fixed styling, added modals, removed routing for /create endpoint in favor of single page for simplicity

This commit is contained in:
Robin K Wilson 2021-07-19 10:21:57 -07:00
Родитель 4ac95414d0
Коммит 52734b47ec
12 изменённых файлов: 302 добавлений и 196 удалений

22
src/react-components/layout/SpinWhileTrue.js поставляемый Normal file
Просмотреть файл

@ -0,0 +1,22 @@
import React from "react";
import PropTypes from "prop-types";
import { Spinner } from "../misc/Spinner";
import { Center } from "./Center";
export function SpinWhileTrue({ isSpinning, children }) {
return (
<>
{isSpinning ? (
<Center>
<Spinner />
</Center>
) : (
<>{children}</>
)}
</>
);
}
SpinWhileTrue.propTypes = {
isSpinning: PropTypes.bool.isRequired
};

Просмотреть файл

@ -16,6 +16,10 @@
margin: 0px !important;
}
:local(.textError) {
color: theme.$red
}
$spacing: (
"2xs": theme.$spacing-2xs,
"xs": theme.$spacing-xs,

32
src/react-components/tokens/CreateToken.js поставляемый
Просмотреть файл

@ -12,6 +12,7 @@ import { CheckboxInput } from "../input/CheckboxInput";
import { RadioInputField } from "../input/RadioInputField";
import { RadioInputOption } from "../input/RadioInput";
import { useIntl, defineMessages } from "react-intl";
import { SpinWhileTrue } from "../layout/SpinWhileTrue";
const tokenTypes = ["account", "app"];
@ -23,18 +24,21 @@ export const CreateToken = ({
toggleTokenType,
error,
showNoScopesSelectedError,
onCreateToken
onCreateToken,
onClose,
isPending
}) => (
<div>
<h1>
<FormattedMessage id="new-token.title" defaultMessage="New Token" />
</h1>
<SpinWhileTrue isSpinning={isPending}>
{error && (
<Row className>
<Row padding="sm" className={styles.revokeWarning}>
<p>{`An Error occured: ${error}`}</p>
</Row>
)}
<Row gap="xl" breakpointColumn="md" topMargin="sm">
<Row gap="xl" breakpointColumn="md" className={styleUtils.smMarginY}>
<h2 className={styleUtils.flexBasis40}>
<FormattedMessage id="new-token.token-type" defaultMessage="Token type" />
</h2>
@ -55,7 +59,7 @@ export const CreateToken = ({
</Row>
</Row>
<Divider />
<Column gap="xl">
<Column gap="xl" className={styleUtils.mdMarginY}>
<h2>
<FormattedMessage id="new-token.select-scopes-title" defaultMessage="Select scopes" />
</h2>
@ -66,7 +70,7 @@ export const CreateToken = ({
/>
</p>
</Column>
<Column>
<Column className={styleUtils.mdMarginY}>
{scopes.map(scopeName => {
return (
<SelectScope
@ -79,12 +83,12 @@ export const CreateToken = ({
})}
</Column>
{showNoScopesSelectedError && (
<Row>
<p>Please select at least one scope.</p>
<Row className={styleUtils.mdMarginY}>
<p className={styleUtils.textError}>Please select at least one scope.</p>
</Row>
)}
<Row spaceBetween className={styleUtils.xlMarginBottom}>
<Button sm preset="basic">
<Button sm preset="basic" onClick={() => onClose({ createdNewToken: false })}>
<FormattedMessage id="new-token.back" defaultMessage="Back" />
</Button>
<Button
@ -95,6 +99,7 @@ export const CreateToken = ({
<FormattedMessage id="new-token.generate" defaultMessage="Generate" />
</Button>
</Row>
</SpinWhileTrue>
</div>
);
@ -106,7 +111,9 @@ CreateToken.propTypes = {
showNoScopesSelectedError: PropTypes.bool,
toggleTokenType: PropTypes.func,
selectedTokenType: PropTypes.string,
onCreateToken: PropTypes.func
onCreateToken: PropTypes.func,
onClose: PropTypes.func,
isPending: PropTypes.bool
};
// Scope info that is localized by language
@ -125,7 +132,7 @@ const scopeInfo = {
defaultMessage: "Read room data"
}
}),
// for storybook example
// For storybook long scope example in Tokens.stories.js
another_long_scope_here: defineMessages({
description: {
id: "new-token-scopes.write-rooms.description",
@ -141,7 +148,10 @@ const SelectScope = ({ scopeName, selected, toggleSelectedScopes }) => {
<Row
padding="sm"
breakpointColumn="md"
className={classNames(styles.backgroundWhite, { [styles.selectedBorder]: selected })}
className={classNames(styles.backgroundWhite, {
[styles.unselectedBorder]: !selected,
[styles.selectedBorder]: selected
})}
topMargin="md"
>
<Row className={classNames(styleUtils.flexBasis40, styles.wordWrap)}>

Просмотреть файл

@ -4,6 +4,7 @@ import PropTypes from "prop-types";
import { CreateToken } from "./CreateToken";
import { RevealTokenModal } from "./RevealTokenModal";
import { createToken, fetchAvailableScopes } from "./token-utils";
import { CenteredModalWrapper } from "../layout/CenteredModalWrapper";
const CreateTokenActions = {
submitCreateToken: "submitCreateToken",
@ -17,22 +18,15 @@ const CreateTokenActions = {
toggleTokenTypeChange: "toggleTokenTypeChange"
};
const steps = {
selectScopes: "selectScopes",
success: "success",
pending: "pending",
error: "error"
};
const initialCreateTokenState = {
step: steps.selectScopes,
scopes: [],
selectedScopes: [],
selectedTokenType: "account",
token: "",
error: "",
showNoScopesSelectedError: false,
showRevealTokenModal: false
showRevealTokenModal: false,
isPending: false
};
function createTokenReducer(state, action) {
@ -41,16 +35,15 @@ function createTokenReducer(state, action) {
console.log(action);
switch (action.type) {
case CreateTokenActions.submitCreateToken:
return { ...state, step: steps.pending };
return { ...state, isPending: true };
case CreateTokenActions.createTokenSuccess:
return { ...state, showRevealTokenModal: true, token: action.token };
return { ...state, isPending: false, showRevealTokenModal: true, token: action.token };
case CreateTokenActions.createTokenError:
return { ...state, error: action.errorMsg };
return { ...state, isPending: false, error: action.errorMsg };
case CreateTokenActions.fetchingScopesSuccess:
console.log("FETCHED SCOPES");
return { ...state, scopes: action.scopes };
case CreateTokenActions.fetchingScopesError:
return { ...state, step: steps.error, error: "Error fetching scopes, please try again later." };
return { ...state, error: "Error fetching scopes, please try again later." };
case CreateTokenActions.showNoScopesError:
return { ...state, showNoScopesSelectedError: true };
case CreateTokenActions.toggleScopeChange: {
@ -76,7 +69,6 @@ function useCreateToken() {
const [state, dispatch] = useReducer(createTokenReducer, initialCreateTokenState);
const onCreateToken = async ({ tokenType, scopes }) => {
// TODO add no scopes error to the view
if (scopes.length === 0) return dispatch({ type: CreateTokenActions.showNoScopesError });
dispatch({ type: CreateTokenActions.submitCreateToken });
@ -112,7 +104,7 @@ function useCreateToken() {
};
return {
step: state.step,
isPending: state.isPending,
scopes: state.scopes,
selectedScopes: state.selectedScopes,
token: state.token,
@ -139,7 +131,8 @@ export const CreateTokenContainer = ({ onClose }) => {
fetchScopes,
toggleSelectedScopes,
toggleTokenType,
selectedTokenType
selectedTokenType,
isPending
} = useCreateToken();
useEffect(
@ -151,7 +144,15 @@ export const CreateTokenContainer = ({ onClose }) => {
return (
<>
{showRevealTokenModal && <RevealTokenModal token={token} selectedScopes={selectedScopes} onClose={onClose} />}
{showRevealTokenModal && (
<CenteredModalWrapper>
<RevealTokenModal
token={token}
selectedScopes={selectedScopes}
onClose={() => onClose({ createdNewToken: true })}
/>
</CenteredModalWrapper>
)}
<CreateToken
showNoScopesSelectedError={showNoScopesSelectedError}
onCreateToken={onCreateToken}
@ -161,7 +162,13 @@ export const CreateTokenContainer = ({ onClose }) => {
toggleSelectedScopes={toggleSelectedScopes}
toggleTokenType={toggleTokenType}
error={error}
onClose={onClose}
isPending={isPending}
/>
</>
);
};
CreateTokenContainer.propTypes = {
onClose: PropTypes.func
};

2
src/react-components/tokens/NoAccess.js поставляемый
Просмотреть файл

@ -2,7 +2,7 @@ import React from "react";
import styles from "./Tokens.scss";
import styleUtils from "../styles/style-utils.scss";
import { Row } from "../layout/Row";
// import { ReactComponent as HubsDuckIcon } from "../../assets/images/footer-duck.svg";
import { ReactComponent as HubsDuckIcon } from "../../assets/images/footer-duck.svg";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Column } from "../layout/Column";
import { FormattedMessage } from "react-intl";

Просмотреть файл

@ -14,7 +14,6 @@ import classNames from "classnames";
import { CopyableTextInputField } from "../input/CopyableTextInputField";
export const RevealTokenModal = ({ token, selectedScopes, onClose }) => {
// TODO add copy token functionality
return (
<Modal
titleNode={

38
src/react-components/tokens/RevokeTokenContainer.js поставляемый Normal file
Просмотреть файл

@ -0,0 +1,38 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
import { Spinner } from "../misc/Spinner";
import { CenteredModalWrapper } from "../layout/CenteredModalWrapper";
import { RevokeTokenModal } from "./RevokeTokenModal";
import { revokeToken } from "./token-utils";
export function RevokeTokenContainer({ onClose, tokenId }) {
const [error, setError] = useState("");
const [isPending, setIsPending] = useState(false);
const onConfirmRevoke = async () => {
setIsPending(true);
try {
await revokeToken({ id: tokenId });
onClose({ removedTokenId: tokenId });
} catch (err) {
setError(err.message);
}
setIsPending(false);
};
return (
<CenteredModalWrapper>
<RevokeTokenModal
isPending={isPending}
error={error}
onRevoke={onConfirmRevoke}
onClose={() => onClose({ removedTokenId: null })}
/>
</CenteredModalWrapper>
);
}
RevokeTokenContainer.propTypes = {
onClose: PropTypes.func,
tokenId: PropTypes.string
};

Просмотреть файл

@ -10,16 +10,27 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faExclamationTriangle } from "@fortawesome/free-solid-svg-icons/faExclamationTriangle";
import { faTimes } from "@fortawesome/free-solid-svg-icons/faTimes";
import { Column } from "../layout/Column";
import { SpinWhileTrue } from "../layout/SpinWhileTrue";
export function RevokeTokenModal({ onClose, onRevoke }) {
export function RevokeTokenModal({ onClose, onRevoke, error, isPending }) {
return (
<Modal
title={<FormattedMessage id="revoke-token-modal.title" defaultMessage="Revoke token" />}
beforeTitle={<FontAwesomeIcon icon={faExclamationTriangle} />}
afterTitle={<FontAwesomeIcon icon={faTimes} onClick={onClose} />}
afterTitle={
<div className={styles.closeModalButton}>
<FontAwesomeIcon onClick={onClose} icon={faTimes} />
</div>
}
disableFullscreen
className={styles.maxWidth400}
>
<SpinWhileTrue isSpinning={isPending}>
{error && (
<Row padding="sm" className={styles.revokeWarning}>
<p>{`An Error occured: ${error}`}</p>
</Row>
)}
<Column padding="sm">
<Column className={styles.revokeDescription}>
<p className={styleUtils.xsMarginBottom}>
@ -56,11 +67,14 @@ export function RevokeTokenModal({ onClose, onRevoke }) {
</Button>
</Row>
</Column>
</SpinWhileTrue>
</Modal>
);
}
RevokeTokenModal.propTypes = {
onClose: PropTypes.func.isRequired,
onRevoke: PropTypes.func.isRequired
onRevoke: PropTypes.func.isRequired,
error: PropTypes.string,
isPending: PropTypes.bool
};

27
src/react-components/tokens/TokenList.js поставляемый
Просмотреть файл

@ -3,14 +3,14 @@ import PropTypes from "prop-types";
import { Token } from "./Token";
import { Row } from "../layout/Row";
import { Column } from "../layout/Column";
import { Center } from "../layout/Center";
import { FormattedMessage } from "react-intl";
import { Button } from "../input/Button";
import styleUtils from "../styles/style-utils.scss";
import styles from "./Tokens.scss";
import { Link } from "react-router-dom";
import { SpinWhileTrue } from "../layout/SpinWhileTrue";
import { Center } from "../layout/Center";
export const TokenList = ({ tokens, onRevokeToken, onCreateToken }) => {
export const TokenList = ({ tokens, onRevokeToken, showCreateToken, error, isFetching }) => {
return (
<div>
<TokenMenuHeader />
@ -18,15 +18,21 @@ export const TokenList = ({ tokens, onRevokeToken, onCreateToken }) => {
<h2>
<FormattedMessage id="empty-token.title2" defaultMessage="Token List" />
</h2>
<Link to="/tokens/create">
<Button preset="primary" sm>
<Button preset="primary" sm onClick={showCreateToken}>
<FormattedMessage id="tokens.button-create-token" defaultMessage="Create token" />
</Button>
</Link>
</Row>
<SpinWhileTrue isSpinning={isFetching}>
{error && (
<Row padding="sm" className={styles.revokeWarning}>
<p>{`An Error occured: ${error}`}</p>
</Row>
)}
{tokens && tokens.length ? (
<Column className={styleUtils.xlMarginY}>
{tokens.map(token => <Token key={token.id} tokenInfo={token} onRevokeToken={onRevokeToken} />)}
{tokens.map(token => (
<Token key={token.id} tokenInfo={token} onRevokeToken={() => onRevokeToken({ revokeId: token.id })} />
))}
</Column>
) : (
<div className={styleUtils.xlMarginY}>
@ -37,16 +43,19 @@ export const TokenList = ({ tokens, onRevokeToken, onCreateToken }) => {
</Row>
</div>
)}
</SpinWhileTrue>
</div>
);
};
TokenList.propTypes = {
tokens: PropTypes.array,
onRevokeToken: PropTypes.func
onRevokeToken: PropTypes.func,
showCreateToken: PropTypes.func,
error: PropTypes.string,
isFetching: PropTypes.bool
};
// TODO move to TokenContainer when defining token state
const TokenMenuHeader = () => (
<Column gap="xl">
<h1>

Просмотреть файл

@ -60,12 +60,16 @@ $tag-top-margin: theme.$spacing-xs;
border: 2px solid theme.$blue;
}
:local(.unselected-border) {
border: 2px solid theme.$background1-color;
}
:local(.word-wrap) {
word-break: break-word;
}
:local(.flex-direction-row) {
flex-direction: row;
flex-direction: row !important;
}
:local(.padding-Y-xl) {
@ -82,7 +86,7 @@ $tag-top-margin: theme.$spacing-xs;
}
:local(.max-width-auto) {
max-width: none;
max-width: none !important;
}
:local(.max-width-400) {

Просмотреть файл

@ -1,77 +1,76 @@
import React, { useState, useEffect, useContext } from "react";
import { Token } from "./Token";
import { fetchMyTokens } from "./token-utils";
import { FormattedMessage } from "react-intl";
import styles from "./Token.scss";
import { RevokeTokenModal } from "./RevokeTokenModal";
import { RevealTokenModal } from "./RevealTokenModal";
import { RevokeTokenContainer } from "./RevokeTokenContainer";
import { TokenList } from "./TokenList";
import { NoAccess } from "./NoAccess";
import { CenteredModalWrapper } from "../layout/CenteredModalWrapper";
import { AuthContext } from "../auth/AuthContext";
import { BrowserRouter, Switch, Route } from "react-router-dom";
import { CreateTokenContainer } from "./CreateTokenContainer";
import { CreateToken } from "./CreateToken";
export function TokensContainer() {
const [tokens, setTokens] = useState([]);
const [showRevealTokenModal, setRevealTokenModal] = useState(false);
// const [showRevokeTokenModal, setShowRevokeTokenModal] = useState(false);
// const [selectedRevokeId, setSelectedRevokeId] = useState();
const [isFetching, setIsFetching] = useState(false);
const [error, setError] = useState("");
const [showCreateToken, setShowCreateToken] = useState(false);
const [showRevokeTokenModal, setShowRevokeTokenModal] = useState(false);
const [selectedRevokeId, setSelectedRevokeId] = useState();
const auth = useContext(AuthContext); // Re-render when you log in/out.
console.log(auth);
console.log(NoAccess);
const fetchTokens = async () => {
try {
setIsFetching(true);
setTokens(await fetchMyTokens());
setIsFetching(false);
} catch (err) {
setError("Error fetching tokens: " + err.message);
setIsFetching(false);
return null;
}
};
useEffect(
() => {
async function updateTokens() {
setTokens(await fetchMyTokens());
await fetchTokens();
}
if (auth?.isAdmin) updateTokens();
},
[auth.isAdmin]
);
const onRevealTokenModalClose = async ({ createdNewToken }) => {
// setRevealTokenModal(false);
// if (createdNewToken) {
// setTokens(await fetchMyTokens());
// }
const onCreateTokenClose = async ({ createdNewToken }) => {
setShowCreateToken(false);
if (createdNewToken) await fetchTokens();
};
const onRevokeToken = ({ revokeId }) => {
setShowRevokeTokenModal(true);
setSelectedRevokeId(revokeId);
};
const onRevokeTokenClose = ({ removedTokenId }) => {
// if (removedTokenId) setTokens(tokens.filter(token => token.id !== removedTokenId));
// setShowRevokeTokenModal(false);
// setSelectedRevokeId("");
if (removedTokenId) setTokens(tokens.filter(token => token.id !== removedTokenId));
setShowRevokeTokenModal(false);
setSelectedRevokeId("");
};
console.log(auth);
return (
<div>
{
// {showRevealTokenModal && (
// <CenteredModalWrapper>
// <RevealTokenModal onClose={onRevealTokenModalClose} />
// </CenteredModalWrapper>
// )}
// {showRevokeTokenModal && (
// <CenteredModalWrapper>
// <RevokeTokenModal selectedId={selectedRevokeId} onClose={onRevokeTokenClose} />
// </CenteredModalWrapper>
// )}
}
{showRevokeTokenModal && <RevokeTokenContainer tokenId={selectedRevokeId} onClose={onRevokeTokenClose} />}
{auth?.isAdmin ? (
<BrowserRouter>
<Switch>
<Route path="/tokens/create">
<CreateTokenContainer />
</Route>
<Route path="/">
<TokenList tokens={tokens} onRevokeToken={onRevokeTokenClose} />
</Route>
</Switch>
</BrowserRouter>
showCreateToken ? (
<CreateTokenContainer onClose={onCreateTokenClose} />
) : (
<TokenList
error={error}
isFetching={isFetching}
tokens={tokens}
onRevokeToken={onRevokeToken}
showCreateToken={() => {
setShowCreateToken(true);
}}
/>
)
) : (
<NoAccess />
)}