From a6a7b7968daefb750cbd49fe0272b4fe13802391 Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 15 Dec 2022 21:55:48 +0000 Subject: [PATCH 01/11] wip --- site/src/AppRouter.tsx | 4 + .../TokensPage/TokensPage.tsx | 30 +++++ .../TokensPage/TokensPageView.tsx | 112 ++++++++++++++++++ 3 files changed, 146 insertions(+) create mode 100644 site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx create mode 100644 site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index 459998d53aa2d..3c7ce3c828d00 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -39,6 +39,9 @@ const SecurityPage = lazy( const SSHKeysPage = lazy( () => import("./pages/UserSettingsPage/SSHKeysPage/SSHKeysPage"), ) +const TokensPage = lazy( + () => import("./pages/UserSettingsPage/TokensPage/TokensPage"), +) const CreateUserPage = lazy( () => import("./pages/UsersPage/CreateUserPage/CreateUserPage"), ) @@ -219,6 +222,7 @@ export const AppRouter: FC = () => { } /> } /> } /> + } /> diff --git a/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx b/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx new file mode 100644 index 0000000000000..c17399b208d09 --- /dev/null +++ b/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx @@ -0,0 +1,30 @@ +import React from "react" +import { Section } from "../../../components/Section/Section" +import { TokensPageView } from "./TokensPageView" + +export const Language = { + title: "Authentication Tokens", + description: ( +

+ The following public key is used to authenticate Git in workspaces. You + may add it to Git services (such as GitHub) that you need to access from + your workspace.
+
+ Coder configures authentication via $GIT_SSH_COMMAND. +

+ ), +} + +export const TokensPage: React.FC> = () => { + return ( + <> +
+ +
+ + ) +} + +export default TokensPage diff --git a/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx b/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx new file mode 100644 index 0000000000000..b9f7a085dc636 --- /dev/null +++ b/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx @@ -0,0 +1,112 @@ +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 { Maybe } from "components/Conditionals/Maybe" +import { Stack } from "components/Stack/Stack" +import { TableEmpty } from "components/TableEmpty/TableEmpty" +import { TableLoader } from "components/TableLoader/TableLoader" +import { FC } from "react" + +export const Language = { + idLabel: "ID", + createdAtLabel: "Created At", + lastUsedLabel: "Last Used", + expiresAtLabel: "Expires At", + emptyMessage: "No tokens found", +} + +export interface TokensPageViewProps { + tokens?: APIKey[] + getTokensError?: Error | unknown +} + +export const TokensPageView: FC< + React.PropsWithChildren +> = ({ + tokens, +}) => { + const theme = useTheme() + + return ( + + + + + + {Language.idLabel} + {Language.createdAtLabel} + {Language.lastUsedLabel} + {Language.expiresAtLabel} + + + + + + + + + + + + + + {tokens?.map((token) => { + + return ( + + + + {token.id} + + + + + + {token.created_at} + + + + + + + {token.last_used} + + + + + + {token.expires_at} + + + + ) + })} + + + +
+
+
+ ) +} From 77835576a782b0ec7e2bae41f4f911ec03064796 Mon Sep 17 00:00:00 2001 From: Garrett Date: Fri, 16 Dec 2022 21:12:44 +0000 Subject: [PATCH 02/11] wip view --- coderd/apikey.go | 6 +-- .../TokensPage/TokensPage.tsx | 48 +++++++++++++++---- .../TokensPage/TokensPageView.tsx | 30 +++++++----- 3 files changed, 59 insertions(+), 25 deletions(-) diff --git a/coderd/apikey.go b/coderd/apikey.go index 5e9aee7c58e66..dea3525965043 100644 --- a/coderd/apikey.go +++ b/coderd/apikey.go @@ -189,10 +189,6 @@ func (api *API) tokens(rw http.ResponseWriter, r *http.Request) { } 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 { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error fetching API keys.", @@ -201,7 +197,7 @@ func (api *API) tokens(rw http.ResponseWriter, r *http.Request) { return } - var apiKeys []codersdk.APIKey + apiKeys := []codersdk.APIKey{} for _, key := range keys { apiKeys = append(apiKeys, convertAPIKey(key)) } diff --git a/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx b/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx index c17399b208d09..4ab9104793aa4 100644 --- a/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx +++ b/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx @@ -1,3 +1,5 @@ +import Button from "@material-ui/core/Button" +import Add from "@material-ui/icons/Add" import React from "react" import { Section } from "../../../components/Section/Section" import { TokensPageView } from "./TokensPageView" @@ -6,11 +8,7 @@ export const Language = { title: "Authentication Tokens", description: (

- The following public key is used to authenticate Git in workspaces. You - may add it to Git services (such as GitHub) that you need to access from - your workspace.
-
- Coder configures authentication via $GIT_SSH_COMMAND. + Authentication tokens are used to authenticate with the Coder API.

), } @@ -18,9 +16,43 @@ export const Language = { export const TokensPage: React.FC> = () => { return ( <> -
- } + > + New Token + + } + > +
diff --git a/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx b/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx index b9f7a085dc636..6c50857d81a24 100644 --- a/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx +++ b/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx @@ -11,6 +11,8 @@ import { Maybe } from "components/Conditionals/Maybe" 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" export const Language = { @@ -39,11 +41,11 @@ export const TokensPageView: FC< - {Language.idLabel} - {Language.createdAtLabel} - {Language.lastUsedLabel} - {Language.expiresAtLabel} - + {Language.idLabel} + {Language.createdAtLabel} + {Language.lastUsedLabel} + {Language.expiresAtLabel} + @@ -52,18 +54,19 @@ export const TokensPageView: FC< - + {tokens?.map((token) => { - + const t = dayjs(token.last_used) + const now = dayjs() + const lastUsed = now.isBefore(t.add(100, "year")) ? t.fromNow() : "Never" return ( @@ -79,24 +82,27 @@ export const TokensPageView: FC< - {token.created_at} + {dayjs(token.created_at).fromNow()} + + {lastUsed} + + - {token.last_used} + {dayjs(token.expires_at).fromNow()} - - {token.expires_at} + From 7297a5013af80a155291fbcef4c640789d513f17 Mon Sep 17 00:00:00 2001 From: Garrett Date: Fri, 16 Dec 2022 21:21:24 +0000 Subject: [PATCH 03/11] lang --- site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx b/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx index 4ab9104793aa4..a9749158085e3 100644 --- a/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx +++ b/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx @@ -5,10 +5,10 @@ import { Section } from "../../../components/Section/Section" import { TokensPageView } from "./TokensPageView" export const Language = { - title: "Authentication Tokens", + title: "Tokens", description: (

- Authentication tokens are used to authenticate with the Coder API. + Tokens are keys used to authenticate with the Coder API.

), } From b2169e5bfbb66beb3e8540a364ceea3b85ced97b Mon Sep 17 00:00:00 2001 From: Garrett Date: Fri, 16 Dec 2022 21:29:37 +0000 Subject: [PATCH 04/11] lang --- site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx b/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx index a9749158085e3..5d99946e2740a 100644 --- a/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx +++ b/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx @@ -8,7 +8,7 @@ export const Language = { title: "Tokens", description: (

- Tokens are keys used to authenticate with the Coder API. + Tokens are used to authenticate with the Coder API.

), } From c64b0b2e875dcc93f269132db40e14da182547d9 Mon Sep 17 00:00:00 2001 From: Garrett Date: Wed, 11 Jan 2023 22:06:29 +0000 Subject: [PATCH 05/11] get deleting working --- site/src/api/api.ts | 14 ++ .../src/components/SettingsLayout/Sidebar.tsx | 9 +- .../TokensPage/TokensPage.tsx | 67 ++++----- .../TokensPage/TokensPageView.tsx | 26 +++- site/src/xServices/tokens/tokensXService.ts | 140 ++++++++++++++++++ 5 files changed, 217 insertions(+), 39 deletions(-) create mode 100644 site/src/xServices/tokens/tokensXService.ts diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 88a19561d436a..4085783b8e4ce 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -133,6 +133,20 @@ export const getApiKey = async (): Promise => { return response.data } +export const getTokens = async (): Promise => { + const response = await axios.get( + "/api/v2/users/me/keys/tokens", + ) + return response.data +} + +export const deleteAPIKey = async (keyId :string): Promise => { + const response = await axios.get( + "/api/v2/users/me/keys/" + keyId, + ) + return response.data +} + export const getUsers = async ( options: TypesGen.UsersRequest, ): Promise => { diff --git a/site/src/components/SettingsLayout/Sidebar.tsx b/site/src/components/SettingsLayout/Sidebar.tsx index ed840fbe6ee70..8e3fc3dc66c0d 100644 --- a/site/src/components/SettingsLayout/Sidebar.tsx +++ b/site/src/components/SettingsLayout/Sidebar.tsx @@ -1,5 +1,6 @@ import { makeStyles } from "@material-ui/core/styles" import VpnKeyOutlined from "@material-ui/icons/VpnKeyOutlined" +import FingerprintOutlinedIcon from '@material-ui/icons/FingerprintOutlined'; import { User } from "api/typesGenerated" import { Stack } from "components/Stack/Stack" import { UserAvatar } from "components/UserAvatar/UserAvatar" @@ -65,10 +66,16 @@ export const Sidebar: React.FC<{ user: User }> = ({ user }) => { } + icon={} > SSH Keys + } + > + Tokens + ) } diff --git a/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx b/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx index 5d99946e2740a..2f92b06d137af 100644 --- a/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx +++ b/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx @@ -1,60 +1,55 @@ -import Button from "@material-ui/core/Button" -import Add from "@material-ui/icons/Add" -import React from "react" +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 { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog" export const Language = { title: "Tokens", description: (

- Tokens are used to authenticate with the Coder API. + Tokens are used to authenticate with the Coder API and can be created with the Coder CLI.

), } -export const TokensPage: React.FC> = () => { +export const TokensPage: FC> = () => { + const [tokensState, tokensSend] = useMachine(tokensMachine) + const isLoading = tokensState.matches("gettingTokens") + const hasLoaded = tokensState.matches("loaded") + const { getTokensError, tokens, deleteTokenId } = tokensState.context + return ( <>
} - > - New Token - - } > { + tokensSend({ type: "DELETE_TOKEN", id }) + }} />
+ + { + tokensSend("CONFIRM_DELETE_TOKEN") + }} + onCancel={() => { + tokensSend("CANCEL_DELETE_TOKEN") + }} + /> ) } diff --git a/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx b/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx index 6c50857d81a24..8478b06d55e9a 100644 --- a/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx +++ b/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx @@ -14,6 +14,9 @@ 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", @@ -21,22 +24,33 @@ export const Language = { lastUsedLabel: "Last Used", expiresAtLabel: "Expires At", emptyMessage: "No tokens found", + ariaDeleteLabel: "Delete Token", } export interface TokensPageViewProps { tokens?: APIKey[] getTokensError?: Error | unknown + isLoading: boolean + hasLoaded: boolean + onDelete: (id: string) => void } export const TokensPageView: FC< React.PropsWithChildren > = ({ tokens, + getTokensError, + isLoading, + hasLoaded, + onDelete, }) => { const theme = useTheme() return ( + {Boolean(getTokensError) && ( + + )}
@@ -49,12 +63,12 @@ export const TokensPageView: FC< - + - + @@ -101,8 +115,16 @@ export const TokensPageView: FC< + { + onDelete(token.id) + }} + size="medium" + aria-label={Language.ariaDeleteLabel} > + diff --git a/site/src/xServices/tokens/tokensXService.ts b/site/src/xServices/tokens/tokensXService.ts new file mode 100644 index 0000000000000..7153b31813e45 --- /dev/null +++ b/site/src/xServices/tokens/tokensXService.ts @@ -0,0 +1,140 @@ +import { getTokens, deleteAPIKey } from "api/api" +import { APIKey } from "api/typesGenerated" +import { displaySuccess } from "components/GlobalSnackbar/utils" +import { createMachine, assign } from "xstate" +import { i18n } from "i18n" + +const { t } = i18n + +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" } + +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( + t("deleteTokenSuccessMessage", { ns: "userSettingsPage" }), + ) + }, + }, + }, +) From f3bc60014a8a880235e308b40ffb710589d622c7 Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 12 Jan 2023 16:19:07 +0000 Subject: [PATCH 06/11] confirm dialog --- site/src/api/api.ts | 6 +- .../src/components/SettingsLayout/Sidebar.tsx | 2 +- .../TokensPage/TokensPage.tsx | 42 +++-- .../TokensPage/TokensPageView.tsx | 157 ++++++++---------- site/src/xServices/tokens/tokensXService.ts | 6 +- 5 files changed, 103 insertions(+), 110 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 4085783b8e4ce..5b32f38b572b5 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -140,10 +140,8 @@ export const getTokens = async (): Promise => { return response.data } -export const deleteAPIKey = async (keyId :string): Promise => { - const response = await axios.get( - "/api/v2/users/me/keys/" + keyId, - ) +export const deleteAPIKey = async (keyId: string): Promise => { + const response = await axios.delete("/api/v2/users/me/keys/" + keyId) return response.data } diff --git a/site/src/components/SettingsLayout/Sidebar.tsx b/site/src/components/SettingsLayout/Sidebar.tsx index 8e3fc3dc66c0d..99e9f471983ae 100644 --- a/site/src/components/SettingsLayout/Sidebar.tsx +++ b/site/src/components/SettingsLayout/Sidebar.tsx @@ -1,6 +1,6 @@ import { makeStyles } from "@material-ui/core/styles" import VpnKeyOutlined from "@material-ui/icons/VpnKeyOutlined" -import FingerprintOutlinedIcon from '@material-ui/icons/FingerprintOutlined'; +import FingerprintOutlinedIcon from "@material-ui/icons/FingerprintOutlined" import { User } from "api/typesGenerated" import { Stack } from "components/Stack/Stack" import { UserAvatar } from "components/UserAvatar/UserAvatar" diff --git a/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx b/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx index 2f92b06d137af..74a9b04409d65 100644 --- a/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx +++ b/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx @@ -3,15 +3,19 @@ import { Section } from "../../../components/Section/Section" import { TokensPageView } from "./TokensPageView" import { tokensMachine } from "xServices/tokens/tokensXService" import { useMachine } from "@xstate/react" -import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog" +import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog" +import { Typography } from "components/Typography/Typography" export const Language = { title: "Tokens", description: (

- Tokens are used to authenticate with the Coder API and can be created with the Coder CLI. + Tokens are used to authenticate with the Coder API and can be created with + the Coder CLI.

), + deleteTitle: "Delete Token", + deleteDescription: "Are you sure you want to delete this token?", } export const TokensPage: FC> = () => { @@ -19,6 +23,14 @@ export const TokensPage: FC> = () => { const isLoading = tokensState.matches("gettingTokens") const hasLoaded = tokensState.matches("loaded") const { getTokensError, tokens, deleteTokenId } = tokensState.context + const content = ( + + {Language.deleteDescription} +
+
+ {deleteTokenId} +
+ ) return ( <> @@ -27,7 +39,7 @@ export const TokensPage: FC> = () => { description={Language.description} layout="fluid" > - > = () => { /> - { - tokensSend("CONFIRM_DELETE_TOKEN") - }} - onCancel={() => { - tokensSend("CANCEL_DELETE_TOKEN") - }} - /> + { + tokensSend("CONFIRM_DELETE_TOKEN") + }} + onClose={() => { + tokensSend("CANCEL_DELETE_TOKEN") + }} + /> ) } diff --git a/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx b/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx index 8478b06d55e9a..549067f70a6ad 100644 --- a/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx +++ b/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx @@ -11,7 +11,7 @@ import { Maybe } from "components/Conditionals/Maybe" 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 DeleteOutlineIcon from "@material-ui/icons/DeleteOutline" import dayjs from "dayjs" import { FC } from "react" import { AlertBanner } from "components/AlertBanner/AlertBanner" @@ -37,13 +37,7 @@ export interface TokensPageViewProps { export const TokensPageView: FC< React.PropsWithChildren -> = ({ - tokens, - getTokensError, - isLoading, - hasLoaded, - onDelete, -}) => { +> = ({ tokens, getTokensError, isLoading, hasLoaded, onDelete }) => { const theme = useTheme() return ( @@ -52,89 +46,78 @@ export const TokensPageView: FC< )} -
- - - {Language.idLabel} - {Language.createdAtLabel} - {Language.lastUsedLabel} - {Language.expiresAtLabel} - - - - - - - +
+ + + {Language.idLabel} + {Language.createdAtLabel} + {Language.lastUsedLabel} + {Language.expiresAtLabel} + + + + + + + - - - - - - {tokens?.map((token) => { - const t = dayjs(token.last_used) - const now = dayjs() - const lastUsed = now.isBefore(t.add(100, "year")) ? t.fromNow() : "Never" - return ( - - - - {token.id} - - + + + + + + {tokens?.map((token) => { + const t = dayjs(token.last_used) + const now = dayjs() + const lastUsed = now.isBefore(t.add(100, "year")) + ? t.fromNow() + : "Never" + return ( + + + + {token.id} + + - - - {dayjs(token.created_at).fromNow()} - - + + + {dayjs(token.created_at).fromNow()} + + + {lastUsed} - - {lastUsed} - - - - - {dayjs(token.expires_at).fromNow()} - - - - - { - onDelete(token.id) - }} - size="medium" - aria-label={Language.ariaDeleteLabel} - > - - - - - - ) - })} - - - -
- + + + {dayjs(token.expires_at).fromNow()} + + + + + { + onDelete(token.id) + }} + size="medium" + aria-label={Language.ariaDeleteLabel} + > + + + + + + ) + })} + + + + + ) } diff --git a/site/src/xServices/tokens/tokensXService.ts b/site/src/xServices/tokens/tokensXService.ts index 7153b31813e45..630e939953a4d 100644 --- a/site/src/xServices/tokens/tokensXService.ts +++ b/site/src/xServices/tokens/tokensXService.ts @@ -14,7 +14,7 @@ interface Context { } type Events = - | { type: "DELETE_TOKEN", id: string } + | { type: "DELETE_TOKEN"; id: string } | { type: "CONFIRM_DELETE_TOKEN" } | { type: "CANCEL_DELETE_TOKEN" } @@ -30,7 +30,7 @@ export const tokensMachine = createMachine( data: APIKey[] } deleteToken: { - data: unknown, + data: unknown } }, }, @@ -101,7 +101,7 @@ export const tokensMachine = createMachine( services: { getTokens: () => getTokens(), deleteToken: (context) => { - if (context.deleteTokenId === undefined) { + if (context.deleteTokenId === undefined) { return Promise.reject("No token id to delete") } From e12e873091cc91025c5f027737e5aa6c7070df49 Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 12 Jan 2023 16:35:56 +0000 Subject: [PATCH 07/11] lang --- site/src/xServices/tokens/tokensXService.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/site/src/xServices/tokens/tokensXService.ts b/site/src/xServices/tokens/tokensXService.ts index 630e939953a4d..2c328341a99d8 100644 --- a/site/src/xServices/tokens/tokensXService.ts +++ b/site/src/xServices/tokens/tokensXService.ts @@ -2,9 +2,6 @@ import { getTokens, deleteAPIKey } from "api/api" import { APIKey } from "api/typesGenerated" import { displaySuccess } from "components/GlobalSnackbar/utils" import { createMachine, assign } from "xstate" -import { i18n } from "i18n" - -const { t } = i18n interface Context { tokens?: APIKey[] @@ -18,6 +15,10 @@ type Events = | { type: "CONFIRM_DELETE_TOKEN" } | { type: "CANCEL_DELETE_TOKEN" } +const Language = { + deleteSuccess: "Token has been deleted", +} + export const tokensMachine = createMachine( { id: "tokensState", @@ -132,7 +133,7 @@ export const tokensMachine = createMachine( }), notifySuccessTokenDeleted: () => { displaySuccess( - t("deleteTokenSuccessMessage", { ns: "userSettingsPage" }), + Language.deleteSuccess, ) }, }, From fce4debfc331ffccca3d04d62d047eb53bc09386 Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 12 Jan 2023 17:05:35 +0000 Subject: [PATCH 08/11] add storybook --- .../TokensPage/TokensPage.tsx | 33 +++++--- .../TokensPage/TokensPageView.stories.tsx | 79 +++++++++++++++++++ .../TokensPage/TokensPageView.tsx | 22 ++++-- site/src/xServices/tokens/tokensXService.ts | 4 +- 4 files changed, 117 insertions(+), 21 deletions(-) create mode 100644 site/src/pages/UserSettingsPage/TokensPage/TokensPageView.stories.tsx diff --git a/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx b/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx index 74a9b04409d65..169464c07f7ca 100644 --- a/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx +++ b/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx @@ -5,15 +5,12 @@ 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", - description: ( -

- Tokens are used to authenticate with the Coder API and can be created with - the Coder CLI. -

- ), + 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?", } @@ -23,6 +20,14 @@ export const TokensPage: FC> = () => { const isLoading = tokensState.matches("gettingTokens") const hasLoaded = tokensState.matches("loaded") const { getTokensError, tokens, deleteTokenId } = tokensState.context + const styles = useStyles() + const description = ( +

+ {Language.descriptionPrefix}{" "} + coder tokens create command. +

+ ) + const content = ( {Language.deleteDescription} @@ -34,11 +39,7 @@ export const TokensPage: FC> = () => { return ( <> -
+
> = () => { ) } +const useStyles = makeStyles((theme) => ({ + code: { + background: theme.palette.divider, + fontSize: 12, + padding: "2px 4px", + color: theme.palette.text.primary, + borderRadius: 2, + }, +})) + export default TokensPage diff --git a/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.stories.tsx b/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.stories.tsx new file mode 100644 index 0000000000000..f9126c31974a6 --- /dev/null +++ b/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.stories.tsx @@ -0,0 +1,79 @@ +import { Story } from "@storybook/react" +import { makeMockApiError } from "testHelpers/entities" +import { TokensPageView, TokensPageViewProps } from "./TokensPageView" + +export default { + title: "components/TokensPageView", + component: TokensPageView, + argTypes: { + onRegenerateClick: { action: "Submit" }, + }, +} + +const Template: Story = (args: TokensPageViewProps) => ( + +) + +export const Example = Template.bind({}) +Example.args = { + isLoading: false, + hasLoaded: true, + tokens: [ + { + 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, + }, + ], + 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.", + }), +} diff --git a/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx b/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx index 549067f70a6ad..4a22eac20ab44 100644 --- a/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx +++ b/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx @@ -7,7 +7,6 @@ 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 { Maybe } from "components/Conditionals/Maybe" import { Stack } from "components/Stack/Stack" import { TableEmpty } from "components/TableEmpty/TableEmpty" import { TableLoader } from "components/TableLoader/TableLoader" @@ -15,7 +14,6 @@ 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 = { @@ -33,11 +31,19 @@ export interface TokensPageViewProps { isLoading: boolean hasLoaded: boolean onDelete: (id: string) => void + deleteTokenError?: Error | unknown } export const TokensPageView: FC< React.PropsWithChildren -> = ({ tokens, getTokensError, isLoading, hasLoaded, onDelete }) => { +> = ({ + tokens, + getTokensError, + isLoading, + hasLoaded, + onDelete, + deleteTokenError, +}) => { const theme = useTheme() return ( @@ -45,6 +51,9 @@ export const TokensPageView: FC< {Boolean(getTokensError) && ( )} + {Boolean(deleteTokenError) && ( + + )} @@ -57,11 +66,10 @@ export const TokensPageView: FC< - - - - + + + diff --git a/site/src/xServices/tokens/tokensXService.ts b/site/src/xServices/tokens/tokensXService.ts index 2c328341a99d8..1bea3af5b9c94 100644 --- a/site/src/xServices/tokens/tokensXService.ts +++ b/site/src/xServices/tokens/tokensXService.ts @@ -132,9 +132,7 @@ export const tokensMachine = createMachine( deleteTokenError: (_) => undefined, }), notifySuccessTokenDeleted: () => { - displaySuccess( - Language.deleteSuccess, - ) + displaySuccess(Language.deleteSuccess) }, }, }, From 8a09fdeaee6ce2bf0924859dfb10137d30bc410e Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 12 Jan 2023 19:25:58 +0000 Subject: [PATCH 09/11] mock tokens --- .../TokensPage/TokensPageView.stories.tsx | 27 ++----------------- .../TokensPage/TokensPageView.tsx | 10 +++---- site/src/testHelpers/entities.ts | 25 +++++++++++++++++ 3 files changed, 32 insertions(+), 30 deletions(-) diff --git a/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.stories.tsx b/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.stories.tsx index f9126c31974a6..9c88c34a34270 100644 --- a/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.stories.tsx +++ b/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.stories.tsx @@ -1,5 +1,5 @@ import { Story } from "@storybook/react" -import { makeMockApiError } from "testHelpers/entities" +import { makeMockApiError, MockTokens } from "testHelpers/entities" import { TokensPageView, TokensPageViewProps } from "./TokensPageView" export default { @@ -18,30 +18,7 @@ export const Example = Template.bind({}) Example.args = { isLoading: false, hasLoaded: true, - tokens: [ - { - 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, - }, - ], + tokens: MockTokens, onDelete: () => { return Promise.resolve() }, diff --git a/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx b/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx index 4a22eac20ab44..a7688f5be3436 100644 --- a/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx +++ b/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx @@ -58,11 +58,11 @@ export const TokensPageView: FC<
- {Language.idLabel} - {Language.createdAtLabel} - {Language.lastUsedLabel} - {Language.expiresAtLabel} - + {Language.idLabel} + {Language.createdAtLabel} + {Language.lastUsedLabel} + {Language.expiresAtLabel} + diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 5dc194dc24f03..ffcd3ea8b8b35 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -20,6 +20,31 @@ export const MockAPIKey: TypesGen.GenerateAPIKeyResponse = { 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 = { external_url: "file:///mock-url", version: "v99.999.9999+c9cdf14", From 24e3f995cada7e3d38bfb976391045efdc6dbcf1 Mon Sep 17 00:00:00 2001 From: Garrett Date: Fri, 13 Jan 2023 17:11:01 +0000 Subject: [PATCH 10/11] add util lastUsedOrNever --- .../TokensPage/TokensPageView.tsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx b/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx index a7688f5be3436..46786b3d57b8f 100644 --- a/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx +++ b/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx @@ -25,6 +25,14 @@ export const Language = { 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 @@ -75,11 +83,6 @@ export const TokensPageView: FC< {tokens?.map((token) => { - const t = dayjs(token.last_used) - const now = dayjs() - const lastUsed = now.isBefore(t.add(100, "year")) - ? t.fromNow() - : "Never" return ( - {lastUsed} + {lastUsedOrNever(token.last_used)} From f885760402fdec3b326f5dee4561171132f87758 Mon Sep 17 00:00:00 2001 From: Garrett Date: Fri, 13 Jan 2023 17:12:41 +0000 Subject: [PATCH 11/11] make fmt --- site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx b/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx index 46786b3d57b8f..3b965cb43ff09 100644 --- a/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx +++ b/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx @@ -28,9 +28,7 @@ export const Language = { const lastUsedOrNever = (lastUsed: string) => { const t = dayjs(lastUsed) const now = dayjs() - return now.isBefore(t.add(100, "year")) - ? t.fromNow() - : "Never" + return now.isBefore(t.add(100, "year")) ? t.fromNow() : "Never" } export interface TokensPageViewProps {