kopia lustrzana https://github.com/bugout-dev/moonstream
frontend changes
rodzic
f68b8f9503
commit
0da4b9561b
|
@ -0,0 +1,97 @@
|
|||
import React, { useState, useEffect, useLayoutEffect } from "react";
|
||||
import TokensList from "../../src/components/TokensList";
|
||||
import TokenRequest from "../../src/components/TokenRequest";
|
||||
import { useTokens } from "../../src/core/hooks";
|
||||
import {
|
||||
VStack,
|
||||
Box,
|
||||
Center,
|
||||
Spinner,
|
||||
ScaleFade,
|
||||
Button,
|
||||
Heading,
|
||||
} from "@chakra-ui/react";
|
||||
import { getLayout } from "../../src/layouts/AccountLayout";
|
||||
|
||||
const Tokens = () => {
|
||||
const [modal, toggleModal] = useState(null);
|
||||
const [newToken, setNewToken] = useState(null);
|
||||
const [tokens, setTokens] = useState();
|
||||
const { list, update, revoke, isLoading, data } = useTokens();
|
||||
|
||||
useEffect(() => {
|
||||
list();
|
||||
//eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (newToken) {
|
||||
const newData = { ...tokens };
|
||||
newData.token.push(newToken);
|
||||
setTokens(newData);
|
||||
setNewToken(null);
|
||||
}
|
||||
}, [newToken, list, data, tokens]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (data?.data?.user_id) {
|
||||
setTokens(data.data);
|
||||
}
|
||||
}, [data?.data, isLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `Tokens`;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{isLoading && !tokens ? (
|
||||
<Center>
|
||||
<Spinner
|
||||
hidden={false}
|
||||
my={8}
|
||||
size="lg"
|
||||
color="primary.500"
|
||||
thickness="4px"
|
||||
speed="1.5s"
|
||||
/>
|
||||
</Center>
|
||||
) : (
|
||||
<ScaleFade in>
|
||||
<Heading variant="tokensScreen"> My access tokens </Heading>
|
||||
<VStack overflow="initial" maxH="unset" height="100%">
|
||||
<Center>
|
||||
<Box h="3rem">
|
||||
{!modal ? (
|
||||
<ScaleFade in={!modal}>
|
||||
<Button
|
||||
onClick={toggleModal}
|
||||
colorScheme="primary"
|
||||
variant="solid"
|
||||
borderRadius="50%"
|
||||
>
|
||||
+
|
||||
</Button>
|
||||
</ScaleFade>
|
||||
) : (
|
||||
<ScaleFade in={modal} unmountOnExit>
|
||||
<TokenRequest toggle={toggleModal} newToken={setNewToken} />
|
||||
</ScaleFade>
|
||||
)}
|
||||
</Box>
|
||||
</Center>
|
||||
<TokensList
|
||||
data={tokens}
|
||||
revoke={revoke}
|
||||
isLoading={isLoading}
|
||||
updateCallback={update}
|
||||
/>
|
||||
</VStack>
|
||||
</ScaleFade>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Tokens.getLayout = getLayout;
|
||||
export default Tokens;
|
|
@ -0,0 +1,57 @@
|
|||
const baseStyle = {
|
||||
fontFamily: "heading",
|
||||
fontWeight: "bold",
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
"4xl": {
|
||||
fontSize: ["6xl", null, "7xl"],
|
||||
lineHeight: 1,
|
||||
},
|
||||
"3xl": {
|
||||
fontSize: ["5xl", null, "6xl"],
|
||||
lineHeight: 1,
|
||||
},
|
||||
"2xl": {
|
||||
fontSize: ["4xl", null, "5xl"],
|
||||
lineHeight: [1.2, null, 1],
|
||||
},
|
||||
xl: {
|
||||
fontSize: ["3xl", null, "4xl"],
|
||||
lineHeight: [1.33, null, 1.2],
|
||||
},
|
||||
lg: {
|
||||
fontSize: ["2xl", null, "3xl"],
|
||||
lineHeight: [1.33, null, 1.2],
|
||||
},
|
||||
md: { fontSize: "xl", lineHeight: 1.2 },
|
||||
sm: { fontSize: "md", lineHeight: 1.2 },
|
||||
xs: { fontSize: "sm", lineHeight: 1.2 },
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
size: "xl",
|
||||
};
|
||||
|
||||
const variantTokensScreen = (props) => {
|
||||
const { colorScheme: c } = props;
|
||||
return {
|
||||
as: "h2",
|
||||
pt: 2,
|
||||
mb: 4,
|
||||
borderBottom: "solid",
|
||||
borderColor: `${c}.50`,
|
||||
borderBottomWidth: "2px",
|
||||
};
|
||||
};
|
||||
|
||||
const variants = {
|
||||
tokensScreen: variantTokensScreen,
|
||||
};
|
||||
|
||||
export default {
|
||||
baseStyle,
|
||||
sizes,
|
||||
defaultProps,
|
||||
variants,
|
||||
};
|
|
@ -10,6 +10,7 @@ import Checkbox from "./Checkbox";
|
|||
import Table from "./Table";
|
||||
import Tooltip from "./Tooltip";
|
||||
import Spinner from "./Spinner";
|
||||
import Heading from "./Heading";
|
||||
import { createBreakpoints } from "@chakra-ui/theme-tools";
|
||||
|
||||
const breakpointsCustom = createBreakpoints({
|
||||
|
@ -57,6 +58,7 @@ const theme = extendTheme({
|
|||
Table,
|
||||
Spinner,
|
||||
Tooltip,
|
||||
Heading,
|
||||
},
|
||||
|
||||
fonts: {
|
||||
|
|
|
@ -35,6 +35,9 @@ const AccountIconButton = (props) => {
|
|||
<RouterLink href="/account/security" passHref>
|
||||
<MenuItem>Security</MenuItem>
|
||||
</RouterLink>
|
||||
<RouterLink href="/account/tokens" passHref>
|
||||
<MenuItem>Access tokens</MenuItem>
|
||||
</RouterLink>
|
||||
</MenuGroup>
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
import React, { useEffect, useRef, Fragment } from "react";
|
||||
import {
|
||||
FormControl,
|
||||
FormErrorMessage,
|
||||
InputGroup,
|
||||
Input,
|
||||
Td,
|
||||
Tr,
|
||||
} from "@chakra-ui/react";
|
||||
import { CloseIcon } from "@chakra-ui/icons";
|
||||
import IconButton from "./IconButton";
|
||||
|
||||
const NewTokenTr = ({ isOpen, toggleSelf, errors, register, journalName }) => {
|
||||
const inputRef = useRef(null);
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
//without timeout input is not catching focus on chrome and firefox..
|
||||
//probably because it is hidden within accordion
|
||||
setTimeout(() => {
|
||||
inputRef.current.focus();
|
||||
}, 100);
|
||||
}
|
||||
}, [inputRef, isOpen]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{isOpen && (
|
||||
<Tr transition="0.3s" _hover={{ bg: "white.200" }}>
|
||||
<Td>New Token:</Td>
|
||||
<Td>
|
||||
<FormControl isInvalid={errors.appName}>
|
||||
<InputGroup>
|
||||
<Input
|
||||
fontSize="sm"
|
||||
border="none"
|
||||
width="60%"
|
||||
defaultValue={journalName}
|
||||
height="fit-content"
|
||||
placeholder="App name"
|
||||
name="appName"
|
||||
ref={(e) => {
|
||||
register(e, { required: "app name is required" });
|
||||
inputRef.current = e;
|
||||
}}
|
||||
/>
|
||||
</InputGroup>
|
||||
<FormErrorMessage color="unsafe.400" pl="1">
|
||||
{errors.appName && errors.appName.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
</Td>
|
||||
<Td>
|
||||
<FormControl isInvalid={errors.appVersion}>
|
||||
<InputGroup>
|
||||
<Input
|
||||
fontSize="sm"
|
||||
border="none"
|
||||
width="60%"
|
||||
height="fit-content"
|
||||
placeholder="App Version"
|
||||
name="appVersion"
|
||||
ref={(e) => {
|
||||
register(e, { required: "app name is required" });
|
||||
}}
|
||||
/>
|
||||
</InputGroup>
|
||||
<FormErrorMessage color="unsafe.400" pl="1">
|
||||
{errors.appVersion && errors.appVersion.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
</Td>
|
||||
<Td>
|
||||
<IconButton type="submit" />
|
||||
<IconButton
|
||||
onClick={() => toggleSelf(false)}
|
||||
icon={<CloseIcon />}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewTokenTr;
|
|
@ -0,0 +1,101 @@
|
|||
import React from "react";
|
||||
import { IconButton } from "@chakra-ui/react";
|
||||
import {
|
||||
Table,
|
||||
Th,
|
||||
Td,
|
||||
Tr,
|
||||
Thead,
|
||||
Tbody,
|
||||
Text,
|
||||
Center,
|
||||
Spinner,
|
||||
} from "@chakra-ui/react";
|
||||
import { DeleteIcon } from "@chakra-ui/icons";
|
||||
import { CopyButton, ConfirmationRequest, NewTokenTr } from ".";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
const TokenList = ({
|
||||
tokens,
|
||||
revoke,
|
||||
isLoading,
|
||||
isNewTokenOpen,
|
||||
toggleNewToken,
|
||||
createToken,
|
||||
journalName,
|
||||
}) => {
|
||||
const { register, handleSubmit, errors } = useForm();
|
||||
if (isLoading)
|
||||
return (
|
||||
<Center>
|
||||
<Spinner />
|
||||
</Center>
|
||||
);
|
||||
|
||||
const handleTokenSubmit = ({ appName, appVersion }) => {
|
||||
createToken({ appName, appVersion }).then(() => toggleNewToken(false));
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleTokenSubmit)}>
|
||||
<Table
|
||||
variant="simple"
|
||||
colorScheme="primary"
|
||||
justifyContent="center"
|
||||
alignItems="baseline"
|
||||
h="auto"
|
||||
size="sm"
|
||||
>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Token</Th>
|
||||
<Th>App Name</Th>
|
||||
<Th>App version</Th>
|
||||
<Th>Action</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{tokens.map((token, idx) => {
|
||||
return (
|
||||
<Tr key={`RestrictedToken-row-${idx}`}>
|
||||
<Td mr={4} p={0}>
|
||||
<CopyButton>{token.restricted_token_id}</CopyButton>
|
||||
</Td>
|
||||
<Td py={0}>{token.app_name}</Td>
|
||||
<Td py={0}>{token.app_version}</Td>
|
||||
<Td py={0}>
|
||||
<ConfirmationRequest
|
||||
bodyMessage={"please confirm"}
|
||||
header={"Delete token"}
|
||||
onConfirm={() => revoke(token.restricted_token_id)}
|
||||
>
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
colorScheme="primary"
|
||||
icon={<DeleteIcon />}
|
||||
/>
|
||||
</ConfirmationRequest>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
|
||||
<NewTokenTr
|
||||
isOpen={isNewTokenOpen}
|
||||
toggleSelf={toggleNewToken}
|
||||
errors={errors}
|
||||
register={register}
|
||||
journalName={journalName}
|
||||
/>
|
||||
</Tbody>
|
||||
</Table>
|
||||
{tokens.length < 1 && (
|
||||
<Center>
|
||||
<Text my={4}>Create Usage report tokens here</Text>
|
||||
</Center>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
export default TokenList;
|
|
@ -0,0 +1,109 @@
|
|||
import React from "react";
|
||||
import {
|
||||
Box,
|
||||
InputGroup,
|
||||
InputLeftElement,
|
||||
FormControl,
|
||||
FormErrorMessage,
|
||||
HStack,
|
||||
Button,
|
||||
InputRightElement,
|
||||
Input,
|
||||
Icon,
|
||||
} from "@chakra-ui/react";
|
||||
import { CloseIcon } from "@chakra-ui/icons";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useUser, useTokens } from "../core/hooks";
|
||||
|
||||
const TokenRequest = ({ newToken, toggle }) => {
|
||||
const { user } = useUser();
|
||||
const { createToken } = useTokens();
|
||||
const { handleSubmit, errors, register } = useForm();
|
||||
const [showPassword, setShowPassword] = useState("password");
|
||||
|
||||
const togglePassword = () => {
|
||||
if (showPassword === "password") {
|
||||
setShowPassword("text");
|
||||
} else {
|
||||
setShowPassword("password");
|
||||
}
|
||||
};
|
||||
|
||||
const PasswordRef = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
if (PasswordRef.current) {
|
||||
PasswordRef.current.focus();
|
||||
}
|
||||
}, [PasswordRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (createToken.data?.data) {
|
||||
newToken(createToken.data.data);
|
||||
toggle(null);
|
||||
}
|
||||
}, [createToken.data, newToken, toggle]);
|
||||
|
||||
const formStyle = {
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
minWidth: "100px",
|
||||
flexFlow: "row wrap-reverse",
|
||||
aligntContent: "flex-end",
|
||||
};
|
||||
|
||||
if (!user) return ""; //loading...
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<form onSubmit={handleSubmit(createToken.mutate)} style={formStyle}>
|
||||
<HStack>
|
||||
<Button
|
||||
variant="solid"
|
||||
colorScheme="primary"
|
||||
type="submit"
|
||||
isLoading={createToken.isLoading}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
|
||||
<FormControl isInvalid={errors.password}>
|
||||
<InputGroup minWidth="300px">
|
||||
<InputLeftElement onClick={togglePassword}>
|
||||
<Icon icon="password" />
|
||||
</InputLeftElement>
|
||||
<Input
|
||||
colorScheme="primary"
|
||||
variant="filled"
|
||||
isDisabled={createToken.isLoading}
|
||||
autoComplete="on"
|
||||
placeholder="Your Bugout password"
|
||||
name="password"
|
||||
type={showPassword}
|
||||
ref={(e) => {
|
||||
register(e, { required: "Password is required!" });
|
||||
PasswordRef.current = e;
|
||||
}}
|
||||
/>
|
||||
<InputRightElement onClick={() => toggle(null)}>
|
||||
<CloseIcon />
|
||||
</InputRightElement>
|
||||
</InputGroup>
|
||||
<FormErrorMessage color="unsafe.400" pl="1" justifyContent="Center">
|
||||
{errors.password && errors.password.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<Input
|
||||
type="hidden"
|
||||
ref={register}
|
||||
name="username"
|
||||
defaultValue={user?.username}
|
||||
/>
|
||||
</HStack>
|
||||
</form>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
export default TokenRequest;
|
|
@ -0,0 +1,88 @@
|
|||
import React from "react";
|
||||
import { Skeleton, IconButton } from "@chakra-ui/react";
|
||||
import {
|
||||
Table,
|
||||
Th,
|
||||
Td,
|
||||
Tr,
|
||||
Thead,
|
||||
Tbody,
|
||||
Editable,
|
||||
EditableInput,
|
||||
EditablePreview,
|
||||
} from "@chakra-ui/react";
|
||||
import { DeleteIcon } from "@chakra-ui/icons";
|
||||
import moment from "moment";
|
||||
import CopyButton from "./CopyButton";
|
||||
|
||||
const List = ({ data, revoke, isLoading, updateCallback }) => {
|
||||
const userToken = localStorage.getItem("BUGOUT_ACCESS_TOKEN");
|
||||
|
||||
if (data) {
|
||||
return (
|
||||
<Table
|
||||
variant="simple"
|
||||
colorScheme="primary"
|
||||
justifyContent="center"
|
||||
alignItems="baseline"
|
||||
h="auto"
|
||||
size="sm"
|
||||
>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Token</Th>
|
||||
<Th>Date Created</Th>
|
||||
<Th>Note</Th>
|
||||
<Th>Actions</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{data.token.map((token) => {
|
||||
if (token.active) {
|
||||
if (userToken !== token.id) {
|
||||
return (
|
||||
<Tr key={`token-row-${token.id}`}>
|
||||
<Td mr={4} p={0}>
|
||||
<CopyButton>{token.id}</CopyButton>
|
||||
</Td>
|
||||
<Td py={0}>{moment(token.created_at).format("L")}</Td>
|
||||
<Td py={0}>
|
||||
<Editable
|
||||
colorScheme="primary"
|
||||
placeholder="enter note here"
|
||||
defaultValue={token.note}
|
||||
onSubmit={(nextValue) =>
|
||||
updateCallback({ token: token.id, note: nextValue })
|
||||
}
|
||||
>
|
||||
<EditablePreview
|
||||
maxW="40rem"
|
||||
_placeholder={{ color: "black" }}
|
||||
/>
|
||||
<EditableInput maxW="40rem" />
|
||||
</Editable>
|
||||
</Td>
|
||||
<Td py={0}>
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
colorScheme="primary"
|
||||
onClick={() => revoke(token.id)}
|
||||
icon={<DeleteIcon />}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
} else return null;
|
||||
} else return null;
|
||||
})}
|
||||
</Tbody>
|
||||
</Table>
|
||||
);
|
||||
} else if (isLoading) {
|
||||
return <Skeleton />;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
export default List;
|
|
@ -1,4 +1,4 @@
|
|||
export { queryCacheProps as hookCommon } from "./hookCommon";
|
||||
export { default as hookCommon } from "./hookCommon";
|
||||
export { default as useAuthResultHandler } from "./useAuthResultHandler";
|
||||
export { default as useChangePassword } from "./useChangePassword";
|
||||
export { default as useClientID } from "./useClientID";
|
||||
|
@ -15,8 +15,10 @@ export { default as useResetPassword } from "./useResetPassword";
|
|||
export { default as useRouter } from "./useRouter";
|
||||
export { default as useSignUp } from "./useSignUp";
|
||||
export { default as useStorage } from "./useStorage";
|
||||
export { default as useStream } from "./useStream";
|
||||
export { default as useStripe } from "./useStripe";
|
||||
export { default as useSubscriptions } from "./useSubscriptions";
|
||||
export { default as useToast } from "./useToast";
|
||||
export { default as useTokens } from "./useTokens";
|
||||
export { default as useTxInfo } from "./useTxInfo";
|
||||
export { default as useUser } from "./useUser";
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
import { useMutation } from "react-query";
|
||||
import { AuthService } from "../services";
|
||||
|
||||
const useTokens = () => {
|
||||
const {
|
||||
mutate: list,
|
||||
isLoading,
|
||||
error,
|
||||
data,
|
||||
} = useMutation(AuthService.getTokenList);
|
||||
const { mutate: revoke } = useMutation(AuthService.revokeToken, {
|
||||
onSuccess: () => {
|
||||
list();
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: update } = useMutation(AuthService.updateToken, {
|
||||
onSuccess: () => {
|
||||
list();
|
||||
},
|
||||
});
|
||||
|
||||
const createToken = useMutation(AuthService.login, {
|
||||
onSuccess: () => {
|
||||
list();
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
createToken,
|
||||
list,
|
||||
update,
|
||||
revoke,
|
||||
isLoading,
|
||||
data,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
export default useTokens;
|
|
@ -74,3 +74,28 @@ export const changePassword = ({ currentPassword, newPassword }) => {
|
|||
data,
|
||||
});
|
||||
};
|
||||
|
||||
export const getTokenList = () => {
|
||||
return http({
|
||||
method: "GET",
|
||||
url: `${AUTH_URL}/tokens`,
|
||||
});
|
||||
};
|
||||
|
||||
export const updateToken = ({ note, token }) => {
|
||||
const data = new FormData();
|
||||
data.append("token_note", note);
|
||||
data.append("access_token", token);
|
||||
return http({
|
||||
method: "PUT",
|
||||
url: `${AUTH_URL}/token`,
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
export const revokeToken = (token) => {
|
||||
return http({
|
||||
method: "POST",
|
||||
url: `${AUTH_URL}/revoke/${token}`,
|
||||
});
|
||||
};
|
||||
|
|
Ładowanie…
Reference in New Issue