mirror of
https://github.com/coder/coder.git
synced 2025-07-09 11:45:56 +00:00
feat: Manage tokens in dashboard (#5444)
This commit is contained in:
@ -189,10 +189,6 @@ func (api *API) tokens(rw http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
keys, err := api.Database.GetAPIKeysByLoginType(ctx, database.LoginTypeToken)
|
keys, err := api.Database.GetAPIKeysByLoginType(ctx, database.LoginTypeToken)
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
|
||||||
httpapi.Write(ctx, rw, http.StatusOK, []codersdk.APIKey{})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||||
Message: "Internal error fetching API keys.",
|
Message: "Internal error fetching API keys.",
|
||||||
@ -201,7 +197,7 @@ func (api *API) tokens(rw http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var apiKeys []codersdk.APIKey
|
apiKeys := []codersdk.APIKey{}
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
apiKeys = append(apiKeys, convertAPIKey(key))
|
apiKeys = append(apiKeys, convertAPIKey(key))
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,9 @@ const SecurityPage = lazy(
|
|||||||
const SSHKeysPage = lazy(
|
const SSHKeysPage = lazy(
|
||||||
() => import("./pages/UserSettingsPage/SSHKeysPage/SSHKeysPage"),
|
() => import("./pages/UserSettingsPage/SSHKeysPage/SSHKeysPage"),
|
||||||
)
|
)
|
||||||
|
const TokensPage = lazy(
|
||||||
|
() => import("./pages/UserSettingsPage/TokensPage/TokensPage"),
|
||||||
|
)
|
||||||
const CreateUserPage = lazy(
|
const CreateUserPage = lazy(
|
||||||
() => import("./pages/UsersPage/CreateUserPage/CreateUserPage"),
|
() => import("./pages/UsersPage/CreateUserPage/CreateUserPage"),
|
||||||
)
|
)
|
||||||
@ -219,6 +222,7 @@ export const AppRouter: FC = () => {
|
|||||||
<Route path="account" element={<AccountPage />} />
|
<Route path="account" element={<AccountPage />} />
|
||||||
<Route path="security" element={<SecurityPage />} />
|
<Route path="security" element={<SecurityPage />} />
|
||||||
<Route path="ssh-keys" element={<SSHKeysPage />} />
|
<Route path="ssh-keys" element={<SSHKeysPage />} />
|
||||||
|
<Route path="tokens" element={<TokensPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="/@:username">
|
<Route path="/@:username">
|
||||||
|
@ -133,6 +133,18 @@ export const getApiKey = async (): Promise<TypesGen.GenerateAPIKeyResponse> => {
|
|||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getTokens = async (): Promise<TypesGen.APIKey[]> => {
|
||||||
|
const response = await axios.get<TypesGen.APIKey[]>(
|
||||||
|
"/api/v2/users/me/keys/tokens",
|
||||||
|
)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteAPIKey = async (keyId: string): Promise<void> => {
|
||||||
|
const response = await axios.delete("/api/v2/users/me/keys/" + keyId)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
export const getUsers = async (
|
export const getUsers = async (
|
||||||
options: TypesGen.UsersRequest,
|
options: TypesGen.UsersRequest,
|
||||||
): Promise<TypesGen.GetUsersResponse> => {
|
): Promise<TypesGen.GetUsersResponse> => {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { makeStyles } from "@material-ui/core/styles"
|
import { makeStyles } from "@material-ui/core/styles"
|
||||||
import VpnKeyOutlined from "@material-ui/icons/VpnKeyOutlined"
|
import VpnKeyOutlined from "@material-ui/icons/VpnKeyOutlined"
|
||||||
|
import FingerprintOutlinedIcon from "@material-ui/icons/FingerprintOutlined"
|
||||||
import { User } from "api/typesGenerated"
|
import { User } from "api/typesGenerated"
|
||||||
import { Stack } from "components/Stack/Stack"
|
import { Stack } from "components/Stack/Stack"
|
||||||
import { UserAvatar } from "components/UserAvatar/UserAvatar"
|
import { UserAvatar } from "components/UserAvatar/UserAvatar"
|
||||||
@ -65,10 +66,16 @@ export const Sidebar: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
</SidebarNavItem>
|
</SidebarNavItem>
|
||||||
<SidebarNavItem
|
<SidebarNavItem
|
||||||
href="ssh-keys"
|
href="ssh-keys"
|
||||||
icon={<SidebarNavItemIcon icon={VpnKeyOutlined} />}
|
icon={<SidebarNavItemIcon icon={FingerprintOutlinedIcon} />}
|
||||||
>
|
>
|
||||||
SSH Keys
|
SSH Keys
|
||||||
</SidebarNavItem>
|
</SidebarNavItem>
|
||||||
|
<SidebarNavItem
|
||||||
|
href="tokens"
|
||||||
|
icon={<SidebarNavItemIcon icon={VpnKeyOutlined} />}
|
||||||
|
>
|
||||||
|
Tokens
|
||||||
|
</SidebarNavItem>
|
||||||
</nav>
|
</nav>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
80
site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx
Normal file
80
site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { FC, PropsWithChildren } from "react"
|
||||||
|
import { Section } from "../../../components/Section/Section"
|
||||||
|
import { TokensPageView } from "./TokensPageView"
|
||||||
|
import { tokensMachine } from "xServices/tokens/tokensXService"
|
||||||
|
import { useMachine } from "@xstate/react"
|
||||||
|
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"
|
||||||
|
import { Typography } from "components/Typography/Typography"
|
||||||
|
import makeStyles from "@material-ui/core/styles/makeStyles"
|
||||||
|
|
||||||
|
export const Language = {
|
||||||
|
title: "Tokens",
|
||||||
|
descriptionPrefix:
|
||||||
|
"Tokens are used to authenticate with the Coder API. You can create a token with the Coder CLI using the ",
|
||||||
|
deleteTitle: "Delete Token",
|
||||||
|
deleteDescription: "Are you sure you want to delete this token?",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TokensPage: FC<PropsWithChildren<unknown>> = () => {
|
||||||
|
const [tokensState, tokensSend] = useMachine(tokensMachine)
|
||||||
|
const isLoading = tokensState.matches("gettingTokens")
|
||||||
|
const hasLoaded = tokensState.matches("loaded")
|
||||||
|
const { getTokensError, tokens, deleteTokenId } = tokensState.context
|
||||||
|
const styles = useStyles()
|
||||||
|
const description = (
|
||||||
|
<p>
|
||||||
|
{Language.descriptionPrefix}{" "}
|
||||||
|
<code className={styles.code}>coder tokens create</code> command.
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<Typography>
|
||||||
|
{Language.deleteDescription}
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
{deleteTokenId}
|
||||||
|
</Typography>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Section title={Language.title} description={description} layout="fluid">
|
||||||
|
<TokensPageView
|
||||||
|
tokens={tokens}
|
||||||
|
isLoading={isLoading}
|
||||||
|
hasLoaded={hasLoaded}
|
||||||
|
getTokensError={getTokensError}
|
||||||
|
onDelete={(id) => {
|
||||||
|
tokensSend({ type: "DELETE_TOKEN", id })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
title={Language.deleteTitle}
|
||||||
|
description={content}
|
||||||
|
open={tokensState.matches("confirmTokenDelete")}
|
||||||
|
confirmLoading={tokensState.matches("deletingToken")}
|
||||||
|
onConfirm={() => {
|
||||||
|
tokensSend("CONFIRM_DELETE_TOKEN")
|
||||||
|
}}
|
||||||
|
onClose={() => {
|
||||||
|
tokensSend("CANCEL_DELETE_TOKEN")
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
code: {
|
||||||
|
background: theme.palette.divider,
|
||||||
|
fontSize: 12,
|
||||||
|
padding: "2px 4px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
borderRadius: 2,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
export default TokensPage
|
@ -0,0 +1,56 @@
|
|||||||
|
import { Story } from "@storybook/react"
|
||||||
|
import { makeMockApiError, MockTokens } from "testHelpers/entities"
|
||||||
|
import { TokensPageView, TokensPageViewProps } from "./TokensPageView"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "components/TokensPageView",
|
||||||
|
component: TokensPageView,
|
||||||
|
argTypes: {
|
||||||
|
onRegenerateClick: { action: "Submit" },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const Template: Story<TokensPageViewProps> = (args: TokensPageViewProps) => (
|
||||||
|
<TokensPageView {...args} />
|
||||||
|
)
|
||||||
|
|
||||||
|
export const Example = Template.bind({})
|
||||||
|
Example.args = {
|
||||||
|
isLoading: false,
|
||||||
|
hasLoaded: true,
|
||||||
|
tokens: MockTokens,
|
||||||
|
onDelete: () => {
|
||||||
|
return Promise.resolve()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Loading = Template.bind({})
|
||||||
|
Loading.args = {
|
||||||
|
...Example.args,
|
||||||
|
isLoading: true,
|
||||||
|
hasLoaded: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Empty = Template.bind({})
|
||||||
|
Empty.args = {
|
||||||
|
...Example.args,
|
||||||
|
tokens: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithGetTokensError = Template.bind({})
|
||||||
|
WithGetTokensError.args = {
|
||||||
|
...Example.args,
|
||||||
|
hasLoaded: false,
|
||||||
|
getTokensError: makeMockApiError({
|
||||||
|
message: "Failed to get tokens.",
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithDeleteTokenError = Template.bind({})
|
||||||
|
WithDeleteTokenError.args = {
|
||||||
|
...Example.args,
|
||||||
|
hasLoaded: false,
|
||||||
|
deleteTokenError: makeMockApiError({
|
||||||
|
message: "Failed to delete token.",
|
||||||
|
}),
|
||||||
|
}
|
132
site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx
Normal file
132
site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import { useTheme } from "@material-ui/core/styles"
|
||||||
|
import Table from "@material-ui/core/Table"
|
||||||
|
import TableBody from "@material-ui/core/TableBody"
|
||||||
|
import TableCell from "@material-ui/core/TableCell"
|
||||||
|
import TableContainer from "@material-ui/core/TableContainer"
|
||||||
|
import TableHead from "@material-ui/core/TableHead"
|
||||||
|
import TableRow from "@material-ui/core/TableRow"
|
||||||
|
import { APIKey } from "api/typesGenerated"
|
||||||
|
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"
|
||||||
|
import { Stack } from "components/Stack/Stack"
|
||||||
|
import { TableEmpty } from "components/TableEmpty/TableEmpty"
|
||||||
|
import { TableLoader } from "components/TableLoader/TableLoader"
|
||||||
|
import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline"
|
||||||
|
import dayjs from "dayjs"
|
||||||
|
import { FC } from "react"
|
||||||
|
import { AlertBanner } from "components/AlertBanner/AlertBanner"
|
||||||
|
import IconButton from "@material-ui/core/IconButton/IconButton"
|
||||||
|
|
||||||
|
export const Language = {
|
||||||
|
idLabel: "ID",
|
||||||
|
createdAtLabel: "Created At",
|
||||||
|
lastUsedLabel: "Last Used",
|
||||||
|
expiresAtLabel: "Expires At",
|
||||||
|
emptyMessage: "No tokens found",
|
||||||
|
ariaDeleteLabel: "Delete Token",
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastUsedOrNever = (lastUsed: string) => {
|
||||||
|
const t = dayjs(lastUsed)
|
||||||
|
const now = dayjs()
|
||||||
|
return now.isBefore(t.add(100, "year")) ? t.fromNow() : "Never"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TokensPageViewProps {
|
||||||
|
tokens?: APIKey[]
|
||||||
|
getTokensError?: Error | unknown
|
||||||
|
isLoading: boolean
|
||||||
|
hasLoaded: boolean
|
||||||
|
onDelete: (id: string) => void
|
||||||
|
deleteTokenError?: Error | unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TokensPageView: FC<
|
||||||
|
React.PropsWithChildren<TokensPageViewProps>
|
||||||
|
> = ({
|
||||||
|
tokens,
|
||||||
|
getTokensError,
|
||||||
|
isLoading,
|
||||||
|
hasLoaded,
|
||||||
|
onDelete,
|
||||||
|
deleteTokenError,
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
{Boolean(getTokensError) && (
|
||||||
|
<AlertBanner severity="error" error={getTokensError} />
|
||||||
|
)}
|
||||||
|
{Boolean(deleteTokenError) && (
|
||||||
|
<AlertBanner severity="error" error={deleteTokenError} />
|
||||||
|
)}
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell width="25%">{Language.idLabel}</TableCell>
|
||||||
|
<TableCell width="25%">{Language.createdAtLabel}</TableCell>
|
||||||
|
<TableCell width="25%">{Language.lastUsedLabel}</TableCell>
|
||||||
|
<TableCell width="25%">{Language.expiresAtLabel}</TableCell>
|
||||||
|
<TableCell width="0%"></TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
<ChooseOne>
|
||||||
|
<Cond condition={isLoading}>
|
||||||
|
<TableLoader />
|
||||||
|
</Cond>
|
||||||
|
<Cond condition={hasLoaded && tokens?.length === 0}>
|
||||||
|
<TableEmpty message={Language.emptyMessage} />
|
||||||
|
</Cond>
|
||||||
|
<Cond>
|
||||||
|
{tokens?.map((token) => {
|
||||||
|
return (
|
||||||
|
<TableRow
|
||||||
|
key={token.id}
|
||||||
|
data-testid={`token-${token.id}`}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<TableCell>
|
||||||
|
<span style={{ color: theme.palette.text.secondary }}>
|
||||||
|
{token.id}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell>
|
||||||
|
<span style={{ color: theme.palette.text.secondary }}>
|
||||||
|
{dayjs(token.created_at).fromNow()}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell>{lastUsedOrNever(token.last_used)}</TableCell>
|
||||||
|
|
||||||
|
<TableCell>
|
||||||
|
<span style={{ color: theme.palette.text.secondary }}>
|
||||||
|
{dayjs(token.expires_at).fromNow()}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span style={{ color: theme.palette.text.secondary }}>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
onDelete(token.id)
|
||||||
|
}}
|
||||||
|
size="medium"
|
||||||
|
aria-label={Language.ariaDeleteLabel}
|
||||||
|
>
|
||||||
|
<DeleteOutlineIcon />
|
||||||
|
</IconButton>
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Cond>
|
||||||
|
</ChooseOne>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
@ -20,6 +20,31 @@ export const MockAPIKey: TypesGen.GenerateAPIKeyResponse = {
|
|||||||
key: "my-api-key",
|
key: "my-api-key",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const MockTokens: TypesGen.APIKey[] = [
|
||||||
|
{
|
||||||
|
id: "tBoVE3dqLl",
|
||||||
|
user_id: "f9ee61d8-1d84-4410-ab6e-c1ec1a641e0b",
|
||||||
|
last_used: "0001-01-01T00:00:00Z",
|
||||||
|
expires_at: "2023-01-15T20:10:45.637438Z",
|
||||||
|
created_at: "2022-12-16T20:10:45.637452Z",
|
||||||
|
updated_at: "2022-12-16T20:10:45.637452Z",
|
||||||
|
login_type: "token",
|
||||||
|
scope: "all",
|
||||||
|
lifetime_seconds: 2592000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "tBoVE3dqLl",
|
||||||
|
user_id: "f9ee61d8-1d84-4410-ab6e-c1ec1a641e0b",
|
||||||
|
last_used: "0001-01-01T00:00:00Z",
|
||||||
|
expires_at: "2023-01-15T20:10:45.637438Z",
|
||||||
|
created_at: "2022-12-16T20:10:45.637452Z",
|
||||||
|
updated_at: "2022-12-16T20:10:45.637452Z",
|
||||||
|
login_type: "token",
|
||||||
|
scope: "all",
|
||||||
|
lifetime_seconds: 2592000,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
export const MockBuildInfo: TypesGen.BuildInfoResponse = {
|
export const MockBuildInfo: TypesGen.BuildInfoResponse = {
|
||||||
external_url: "file:///mock-url",
|
external_url: "file:///mock-url",
|
||||||
version: "v99.999.9999+c9cdf14",
|
version: "v99.999.9999+c9cdf14",
|
||||||
|
139
site/src/xServices/tokens/tokensXService.ts
Normal file
139
site/src/xServices/tokens/tokensXService.ts
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import { getTokens, deleteAPIKey } from "api/api"
|
||||||
|
import { APIKey } from "api/typesGenerated"
|
||||||
|
import { displaySuccess } from "components/GlobalSnackbar/utils"
|
||||||
|
import { createMachine, assign } from "xstate"
|
||||||
|
|
||||||
|
interface Context {
|
||||||
|
tokens?: APIKey[]
|
||||||
|
getTokensError?: unknown
|
||||||
|
deleteTokenError?: unknown
|
||||||
|
deleteTokenId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Events =
|
||||||
|
| { type: "DELETE_TOKEN"; id: string }
|
||||||
|
| { type: "CONFIRM_DELETE_TOKEN" }
|
||||||
|
| { type: "CANCEL_DELETE_TOKEN" }
|
||||||
|
|
||||||
|
const Language = {
|
||||||
|
deleteSuccess: "Token has been deleted",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const tokensMachine = createMachine(
|
||||||
|
{
|
||||||
|
id: "tokensState",
|
||||||
|
predictableActionArguments: true,
|
||||||
|
schema: {
|
||||||
|
context: {} as Context,
|
||||||
|
events: {} as Events,
|
||||||
|
services: {} as {
|
||||||
|
getTokens: {
|
||||||
|
data: APIKey[]
|
||||||
|
}
|
||||||
|
deleteToken: {
|
||||||
|
data: unknown
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tsTypes: {} as import("./tokensXService.typegen").Typegen0,
|
||||||
|
initial: "gettingTokens",
|
||||||
|
states: {
|
||||||
|
gettingTokens: {
|
||||||
|
entry: "clearGetTokensError",
|
||||||
|
invoke: {
|
||||||
|
src: "getTokens",
|
||||||
|
onDone: [
|
||||||
|
{
|
||||||
|
actions: "assignTokens",
|
||||||
|
target: "loaded",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onError: [
|
||||||
|
{
|
||||||
|
actions: "assignGetTokensError",
|
||||||
|
target: "notLoaded",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
notLoaded: {
|
||||||
|
type: "final",
|
||||||
|
},
|
||||||
|
loaded: {
|
||||||
|
on: {
|
||||||
|
DELETE_TOKEN: {
|
||||||
|
actions: "assignDeleteTokenId",
|
||||||
|
target: "confirmTokenDelete",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
confirmTokenDelete: {
|
||||||
|
on: {
|
||||||
|
CANCEL_DELETE_TOKEN: {
|
||||||
|
actions: "clearDeleteTokenId",
|
||||||
|
target: "loaded",
|
||||||
|
},
|
||||||
|
CONFIRM_DELETE_TOKEN: {
|
||||||
|
target: "deletingToken",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
deletingToken: {
|
||||||
|
entry: "clearDeleteTokenError",
|
||||||
|
invoke: {
|
||||||
|
src: "deleteToken",
|
||||||
|
onDone: [
|
||||||
|
{
|
||||||
|
actions: ["clearDeleteTokenId", "notifySuccessTokenDeleted"],
|
||||||
|
target: "gettingTokens",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onError: [
|
||||||
|
{
|
||||||
|
actions: ["clearDeleteTokenId", "assignDeleteTokenError"],
|
||||||
|
target: "loaded",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
services: {
|
||||||
|
getTokens: () => getTokens(),
|
||||||
|
deleteToken: (context) => {
|
||||||
|
if (context.deleteTokenId === undefined) {
|
||||||
|
return Promise.reject("No token id to delete")
|
||||||
|
}
|
||||||
|
|
||||||
|
return deleteAPIKey(context.deleteTokenId)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
assignTokens: assign({
|
||||||
|
tokens: (_, { data }) => data,
|
||||||
|
}),
|
||||||
|
assignGetTokensError: assign({
|
||||||
|
getTokensError: (_, { data }) => data,
|
||||||
|
}),
|
||||||
|
clearGetTokensError: assign({
|
||||||
|
getTokensError: (_) => undefined,
|
||||||
|
}),
|
||||||
|
assignDeleteTokenId: assign({
|
||||||
|
deleteTokenId: (_, event) => event.id,
|
||||||
|
}),
|
||||||
|
clearDeleteTokenId: assign({
|
||||||
|
deleteTokenId: (_) => undefined,
|
||||||
|
}),
|
||||||
|
assignDeleteTokenError: assign({
|
||||||
|
deleteTokenError: (_, { data }) => data,
|
||||||
|
}),
|
||||||
|
clearDeleteTokenError: assign({
|
||||||
|
deleteTokenError: (_) => undefined,
|
||||||
|
}),
|
||||||
|
notifySuccessTokenDeleted: () => {
|
||||||
|
displaySuccess(Language.deleteSuccess)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
Reference in New Issue
Block a user