Skip to content

Commit 0203af2

Browse files
committed
get deleting working
1 parent 4500b71 commit 0203af2

File tree

5 files changed

+217
-39
lines changed

5 files changed

+217
-39
lines changed

site/src/api/api.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,20 @@ export const getApiKey = async (): Promise<TypesGen.GenerateAPIKeyResponse> => {
133133
return response.data
134134
}
135135

136+
export const getTokens = async (): Promise<TypesGen.APIKey[]> => {
137+
const response = await axios.get<TypesGen.APIKey[]>(
138+
"/api/v2/users/me/keys/tokens",
139+
)
140+
return response.data
141+
}
142+
143+
export const deleteAPIKey = async (keyId :string): Promise<void> => {
144+
const response = await axios.get(
145+
"/api/v2/users/me/keys/" + keyId,
146+
)
147+
return response.data
148+
}
149+
136150
export const getUsers = async (
137151
options: TypesGen.UsersRequest,
138152
): Promise<TypesGen.GetUsersResponse> => {

site/src/components/SettingsLayout/Sidebar.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { makeStyles } from "@material-ui/core/styles"
22
import VpnKeyOutlined from "@material-ui/icons/VpnKeyOutlined"
3+
import FingerprintOutlinedIcon from '@material-ui/icons/FingerprintOutlined';
34
import { User } from "api/typesGenerated"
45
import { Stack } from "components/Stack/Stack"
56
import { UserAvatar } from "components/UserAvatar/UserAvatar"
@@ -65,10 +66,16 @@ export const Sidebar: React.FC<{ user: User }> = ({ user }) => {
6566
</SidebarNavItem>
6667
<SidebarNavItem
6768
href="../ssh-keys"
68-
icon={<SidebarNavItemIcon icon={VpnKeyOutlined} />}
69+
icon={<SidebarNavItemIcon icon={FingerprintOutlinedIcon} />}
6970
>
7071
SSH Keys
7172
</SidebarNavItem>
73+
<SidebarNavItem
74+
href="../tokens"
75+
icon={<SidebarNavItemIcon icon={VpnKeyOutlined} />}
76+
>
77+
Tokens
78+
</SidebarNavItem>
7279
</nav>
7380
)
7481
}

site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx

Lines changed: 31 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,55 @@
1-
import Button from "@material-ui/core/Button"
2-
import Add from "@material-ui/icons/Add"
3-
import React from "react"
1+
import { FC, PropsWithChildren } from "react"
42
import { Section } from "../../../components/Section/Section"
53
import { TokensPageView } from "./TokensPageView"
4+
import { tokensMachine } from "xServices/tokens/tokensXService"
5+
import { useMachine } from "@xstate/react"
6+
import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog"
67

78
export const Language = {
89
title: "Tokens",
910
description: (
1011
<p>
11-
Tokens are used to authenticate with the Coder API.
12+
Tokens are used to authenticate with the Coder API and can be created with the Coder CLI.
1213
</p>
1314
),
1415
}
1516

16-
export const TokensPage: React.FC<React.PropsWithChildren<unknown>> = () => {
17+
export const TokensPage: FC<PropsWithChildren<unknown>> = () => {
18+
const [tokensState, tokensSend] = useMachine(tokensMachine)
19+
const isLoading = tokensState.matches("gettingTokens")
20+
const hasLoaded = tokensState.matches("loaded")
21+
const { getTokensError, tokens, deleteTokenId } = tokensState.context
22+
1723
return (
1824
<>
1925
<Section
2026
title={Language.title}
2127
description={Language.description}
2228
layout="fluid"
23-
toolbar={
24-
<Button
25-
startIcon={<Add />}
26-
>
27-
New Token
28-
</Button>
29-
}
3029
>
3130
<TokensPageView
32-
tokens={[
33-
{
34-
"id":"tBoVE3dqLl",
35-
"user_id":"f9ee61d8-1d84-4410-ab6e-c1ec1a641e0b",
36-
"last_used":"0001-01-01T00:00:00Z",
37-
"expires_at":"2023-01-15T20:10:45.637438Z",
38-
"created_at":"2022-12-16T20:10:45.637452Z",
39-
"updated_at":"2022-12-16T20:10:45.637452Z",
40-
"login_type":"token",
41-
"scope":"all",
42-
"lifetime_seconds":2592000,
43-
},
44-
{
45-
"id":"tBoVE3dqLl",
46-
"user_id":"f9ee61d8-1d84-4410-ab6e-c1ec1a641e0b",
47-
"last_used":"0001-01-01T00:00:00Z",
48-
"expires_at":"2023-01-15T20:10:45.637438Z",
49-
"created_at":"2022-12-16T20:10:45.637452Z",
50-
"updated_at":"2022-12-16T20:10:45.637452Z",
51-
"login_type":"token",
52-
"scope":"all",
53-
"lifetime_seconds":2592000,
54-
}
55-
]}
31+
tokens={tokens}
32+
isLoading={isLoading}
33+
hasLoaded={hasLoaded}
34+
getTokensError={getTokensError}
35+
onDelete={(id) => {
36+
tokensSend({ type: "DELETE_TOKEN", id })
37+
}}
5638
/>
5739
</Section>
40+
41+
<DeleteDialog
42+
isOpen={tokensState.matches("confirmTokenDelete")}
43+
confirmLoading={tokensState.matches("deletingToken")}
44+
name={deleteTokenId ? deleteTokenId : ""}
45+
entity="token"
46+
onConfirm={() => {
47+
tokensSend("CONFIRM_DELETE_TOKEN")
48+
}}
49+
onCancel={() => {
50+
tokensSend("CANCEL_DELETE_TOKEN")
51+
}}
52+
/>
5853
</>
5954
)
6055
}

site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,29 +14,43 @@ import { TableLoader } from "components/TableLoader/TableLoader"
1414
import DeleteOutlineIcon from '@material-ui/icons/DeleteOutline';
1515
import dayjs from "dayjs"
1616
import { FC } from "react"
17+
import { AlertBanner } from "components/AlertBanner/AlertBanner"
18+
19+
import IconButton from "@material-ui/core/IconButton/IconButton"
1720

1821
export const Language = {
1922
idLabel: "ID",
2023
createdAtLabel: "Created At",
2124
lastUsedLabel: "Last Used",
2225
expiresAtLabel: "Expires At",
2326
emptyMessage: "No tokens found",
27+
ariaDeleteLabel: "Delete Token",
2428
}
2529

2630
export interface TokensPageViewProps {
2731
tokens?: APIKey[]
2832
getTokensError?: Error | unknown
33+
isLoading: boolean
34+
hasLoaded: boolean
35+
onDelete: (id: string) => void
2936
}
3037

3138
export const TokensPageView: FC<
3239
React.PropsWithChildren<TokensPageViewProps>
3340
> = ({
3441
tokens,
42+
getTokensError,
43+
isLoading,
44+
hasLoaded,
45+
onDelete,
3546
}) => {
3647
const theme = useTheme()
3748

3849
return (
3950
<Stack>
51+
{Boolean(getTokensError) && (
52+
<AlertBanner severity="error" error={getTokensError} />
53+
)}
4054
<TableContainer>
4155
<Table>
4256
<TableHead>
@@ -49,12 +63,12 @@ export const TokensPageView: FC<
4963
</TableRow>
5064
</TableHead>
5165
<TableBody>
52-
<Maybe condition={tokens === undefined}>
66+
<Maybe condition={isLoading}>
5367
<TableLoader />
5468
</Maybe>
5569

5670
<ChooseOne>
57-
<Cond condition={tokens?.length === 0}>
71+
<Cond condition={hasLoaded && tokens?.length === 0}>
5872
<TableEmpty
5973
message={Language.emptyMessage}
6074
/>
@@ -101,8 +115,16 @@ export const TokensPageView: FC<
101115
<TableCell>
102116
<span
103117
style={{ color: theme.palette.text.secondary }}
118+
>
119+
<IconButton
120+
onClick={() => {
121+
onDelete(token.id)
122+
}}
123+
size="medium"
124+
aria-label={Language.ariaDeleteLabel}
104125
>
105126
<DeleteOutlineIcon />
127+
</IconButton>
106128
</span>
107129
</TableCell>
108130
</TableRow>
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { getTokens, deleteAPIKey } from "api/api"
2+
import { APIKey } from "api/typesGenerated"
3+
import { displaySuccess } from "components/GlobalSnackbar/utils"
4+
import { createMachine, assign } from "xstate"
5+
import { i18n } from "i18n"
6+
7+
const { t } = i18n
8+
9+
interface Context {
10+
tokens?: APIKey[]
11+
getTokensError?: unknown
12+
deleteTokenError?: unknown
13+
deleteTokenId?: string
14+
}
15+
16+
type Events =
17+
| { type: "DELETE_TOKEN", id: string }
18+
| { type: "CONFIRM_DELETE_TOKEN" }
19+
| { type: "CANCEL_DELETE_TOKEN" }
20+
21+
export const tokensMachine = createMachine(
22+
{
23+
id: "tokensState",
24+
predictableActionArguments: true,
25+
schema: {
26+
context: {} as Context,
27+
events: {} as Events,
28+
services: {} as {
29+
getTokens: {
30+
data: APIKey[]
31+
}
32+
deleteToken: {
33+
data: unknown,
34+
}
35+
},
36+
},
37+
tsTypes: {} as import("./tokensXService.typegen").Typegen0,
38+
initial: "gettingTokens",
39+
states: {
40+
gettingTokens: {
41+
entry: "clearGetTokensError",
42+
invoke: {
43+
src: "getTokens",
44+
onDone: [
45+
{
46+
actions: "assignTokens",
47+
target: "loaded",
48+
},
49+
],
50+
onError: [
51+
{
52+
actions: "assignGetTokensError",
53+
target: "notLoaded",
54+
},
55+
],
56+
},
57+
},
58+
notLoaded: {
59+
type: "final",
60+
},
61+
loaded: {
62+
on: {
63+
DELETE_TOKEN: {
64+
actions: "assignDeleteTokenId",
65+
target: "confirmTokenDelete",
66+
},
67+
},
68+
},
69+
confirmTokenDelete: {
70+
on: {
71+
CANCEL_DELETE_TOKEN: {
72+
actions: "clearDeleteTokenId",
73+
target: "loaded",
74+
},
75+
CONFIRM_DELETE_TOKEN: {
76+
target: "deletingToken",
77+
},
78+
},
79+
},
80+
deletingToken: {
81+
entry: "clearDeleteTokenError",
82+
invoke: {
83+
src: "deleteToken",
84+
onDone: [
85+
{
86+
actions: ["clearDeleteTokenId", "notifySuccessTokenDeleted"],
87+
target: "gettingTokens",
88+
},
89+
],
90+
onError: [
91+
{
92+
actions: ["clearDeleteTokenId", "assignDeleteTokenError"],
93+
target: "loaded",
94+
},
95+
],
96+
},
97+
},
98+
},
99+
},
100+
{
101+
services: {
102+
getTokens: () => getTokens(),
103+
deleteToken: (context) => {
104+
if (context.deleteTokenId === undefined) {
105+
return Promise.reject("No token id to delete")
106+
}
107+
108+
return deleteAPIKey(context.deleteTokenId)
109+
},
110+
},
111+
actions: {
112+
assignTokens: assign({
113+
tokens: (_, { data }) => data,
114+
}),
115+
assignGetTokensError: assign({
116+
getTokensError: (_, { data }) => data,
117+
}),
118+
clearGetTokensError: assign({
119+
getTokensError: (_) => undefined,
120+
}),
121+
assignDeleteTokenId: assign({
122+
deleteTokenId: (_, event) => event.id,
123+
}),
124+
clearDeleteTokenId: assign({
125+
deleteTokenId: (_) => undefined,
126+
}),
127+
assignDeleteTokenError: assign({
128+
deleteTokenError: (_, { data }) => data,
129+
}),
130+
clearDeleteTokenError: assign({
131+
deleteTokenError: (_) => undefined,
132+
}),
133+
notifySuccessTokenDeleted: () => {
134+
displaySuccess(
135+
t("deleteTokenSuccessMessage", { ns: "userSettingsPage" }),
136+
)
137+
},
138+
},
139+
},
140+
)

0 commit comments

Comments
 (0)