8000 feat(site): add ability to create tokens from account tokens page by Kira-Pilot · Pull Request #6608 · coder/coder · GitHub
[go: up one dir, main page]

Skip to content

feat(site): add ability to create tokens from account tokens page #6608

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Mar 16, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
removed token switch
  • Loading branch information
Kira-Pilot committed Mar 10, 2023
commit 482f230a9f5f316ceeaaec5d6e701f94a74ec111
1 change: 0 additions & 1 deletion coderd/apikey.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ import (
// @Success 201 {object} codersdk.GenerateAPIKeyResponse
// @Router /users/{user}/keys/tokens [post]
func (api *API) postToken(rw http.ResponseWriter, r *http.Request) {
fmt.Println("in postToken!")
ctx := r.Context()
user := httpmw.UserParam(r)

Expand Down
14 changes: 9 additions & 5 deletions site/src/i18n/en/tokensPage.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,14 @@
"deleteCaption": "Are you sure you want to delete this token?<br/><br/><4>{{tokenId}}</4>",
"deleteSuccess": "Token has been deleted",
"deleteFailure": "Failed to delete token"
},
"toggleLabel": "Show all tokens"
}
},
"table": {
"id": "ID",
"name": "Name",
"lastUsed": "Last Used",
"expiresAt": "Expires At",
"createdAt": "Created At",
"owner": "Owner"
"createdAt": "Created At"
},
"createToken": {
"title": "Create Token",
Expand All @@ -29,7 +27,13 @@
},
"lifetimeSection": {
"title": "Expiration",
"description": "Fill me out"
"description": "The token will expire on {{date}}.",
"7": "7 days",
"30": "30 days",
"60": "60 days",
"90": "90 days",
"custom": "Custom",
"noExpiration": "No expiration"
},
"fields": {
"name": "Name",
Expand Down
53 changes: 43 additions & 10 deletions site/src/pages/CreateTokenPage/CreateTokenPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FC, useState } from "react"
import { FC, useEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import { Helmet } from "react-helmet-async"
import { pageTitle } from "util/page"
Expand All @@ -17,14 +17,36 @@ import MenuItem from "@material-ui/core/MenuItem"
import { displaySuccess, displayError } from "components/GlobalSnackbar/utils"
import { useMutation } from "@tanstack/react-query"
import { createToken } from "api/api"
import i18next from "i18next"
import dayjs from "dayjs"

const NANO_HOUR = 3600000000000

const lifetimes = [
{ label: "7 days", lifetimeDays: 7 },
{ label: "30 days", lifetimeDays: 30 },
{ label: "60 days", lifetimeDays: 60 },
{ label: "90 days", lifetimeDays: 60 },
{
label: i18next.t("tokensPage:createToken.lifetimeSection.7"),
lifetimeDays: 7,
},
{
label: i18next.t("tokensPage:createToken.lifetimeSection.30"),
lifetimeDays: 30,
},
{
label: i18next.t("tokensPage:createToken.lifetimeSection.60"),
lifetimeDays: 60,
},
{
label: i18next.t("tokensPage:createToken.lifetimeSection.90"),
lifetimeDays: 90,
},
{
label: i18next.t("tokensPage:createToken.lifetimeSection.custom"),
lifetimeDays: 120, // fix
},
{
label: i18next.t("tokensPage:createToken.lifetimeSection.noExpiration"),
lifetimeDays: 365 * 290, // fix
},
]

interface CreateTokenData {
Expand All @@ -42,7 +64,10 @@ const CreateTokenPage: FC = () => {
const navigate = useNavigate()
const navigateBack = () => navigate(-1)
const useCreateToken = () => useMutation(createToken)
const [formErr, setFormErr] = useState<unknown | undefined>(undefined)
const [formError, setFormError] = useState<unknown | undefined>(undefined)
const [expDate, setExpDate] = useState<string>(
dayjs().add(initialValues.lifetime, "days").utc().format("MMMM DD, YYYY"),
)

const { mutate: saveToken, isLoading, isError } = useCreateToken()

Expand All @@ -52,7 +77,7 @@ const CreateTokenPage: FC = () => {
}

const onCreateError = (error: unknown) => {
setFormErr(error)
setFormError(error)
displayError(t("createToken.createError"))
}

10000 Expand All @@ -73,7 +98,13 @@ const CreateTokenPage: FC = () => {
},
})

const getFieldHelpers = getFormHelpers<CreateTokenData>(form, formErr)
useEffect(() => {
setExpDate(
dayjs().add(form.values.lifetime, "days").utc().format("MMMM DD, YYYY"),
)
}, [form.values.lifetime])

const getFieldHelpers = getFormHelpers<CreateTokenData>(form, formError)

return (
<>
Expand All @@ -92,7 +123,7 @@ const CreateTokenPage: FC = () => {
<FormFields>
<TextField
{...getFieldHelpers("name")}
onChange={onChangeTrimmed(form)}
onChange={onChangeTrimmed(form, () => setFormError(undefined))}
autoFocus
fullWidth
required
Expand All @@ -103,7 +134,9 @@ const CreateTokenPage: FC = () => {
</FormSection>
<FormSection
title={t("createToken.lifetimeSection.title")}
description={t("createToken.lifetimeSection.description")}
description={t("createToken.lifetimeSection.description", {
date: expDate,
})}
>
<FormFields>
<TextField
Expand Down
16 changes: 5 additions & 11 deletions site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { Section } from "components/SettingsLayout/Section"
import { TokensPageView } from "./TokensPageView"
import makeStyles from "@material-ui/core/styles/makeStyles"
import { useTranslation, Trans } from "react-i18next"
import { useTokensData, useCheckTokenPermissions } from "./hooks"
import { TokensSwitch, ConfirmDeleteDialog } from "./components"
import { useTokensData } from "./hooks"
import { ConfirmDeleteDialog } from "./components"
import { Stack } from "components/Stack/Stack"
import Button from "@material-ui/core/Button"
import { Link as RouterLink } from "react-router-dom"
Expand All @@ -24,11 +24,6 @@ export const TokensPage: FC<PropsWithChildren<unknown>> = () => {

const TokenActions = () => (
<Stack direction="row" justifyContent="end" className={styles.tokenActions}>
<TokensSwitch
hasReadAll={perms?.readAllApiKeys ?? false}
viewAllTokens={viewAllTokens}
setViewAllTokens={setViewAllTokens}
/>
<Button startIcon={<AddIcon />} component={RouterLink} to="new">
{t("tokenActions.addToken")}
</Button>
Expand All @@ -38,8 +33,6 @@ export const TokensPage: FC<PropsWithChildren<unknown>> = () => {
const [tokenIdToDelete, setTokenIdToDelete] = useState<string | undefined>(
undefined,
)
const [viewAllTokens, setViewAllTokens] = useState<boolean>(false)
const { data: perms } = useCheckTokenPermissions()

const {
data: tokens,
Expand All @@ -48,7 +41,9 @@ export const TokensPage: FC<PropsWithChildren<unknown>> = () => {
isFetched,
queryKey,
} = useTokensData({
include_all: viewAllTokens,
// we currently do not show all tokens in the UI, even if
// the user has read all permissions
include_all: false,
})

return (
Expand All @@ -62,7 +57,6 @@ export const TokensPage: FC<PropsWithChildren<unknown>> = () => {
<TokenActions />
<TokensPageView
tokens={tokens}
viewAllTokens={viewAllTokens}
isLoading={isFetching}
hasLoaded={isFetched}
getTokensError={getTokensError}
Expand Down
23 changes: 5 additions & 18 deletions site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ const lastUsedOrNever = (lastUsed: string) => {

export interface TokensPageViewProps {
tokens?: APIKeyWithOwner[]
viewAllTokens: boolean
getTokensError?: Error | unknown
isLoading: boolean
hasLoaded: boolean
Expand All @@ -37,7 +36,6 @@ export const TokensPageView: FC<
React.PropsWithChildren<TokensPageViewProps>
> = ({
tokens,
viewAllTokens,
getTokensError,
isLoading,
hasLoaded,
Expand All @@ -46,7 +44,6 @@ export const TokensPageView: FC<
}) => {
const theme = useTheme()
const { t } = useTranslation("tokensPage")
const colWidth = viewAllTokens ? "16.66%" : "20%"

return (
<Stack>
Expand All @@ -60,14 +57,11 @@ export const TokensPageView: FC<
<Table>
<TableHead>
<TableRow>
<TableCell width={colWidth}>{t("table.id")}</TableCell>
<TableCell width={colWidth}>{t("table.name")}</TableCell>
<TableCell width={colWidth}>{t("table.lastUsed")}</TableCell>
<TableCell width={colWidth}>{t("table.expiresAt")}</TableCell>
<TableCell width={colWidth}>{t("table.createdAt")}</TableCell>
{viewAllTokens && (
<TableCell width="20%">{t("table.owner")}</TableCell>
)}
<TableCell width="20%">{t("table.id")}</TableCell>
<TableCell width="20%">{t("table.name")}</TableCell>
<TableCell width="20%">{t("table.lastUsed")}</TableCell>
<TableCell width="20%">{t("table.expiresAt")}</TableCell>
<TableCell width="20%">{t("table.createdAt")}</TableCell>
<TableCell width="0%"></TableCell>
</TableRow>
</TableHead>
Expand Down Expand Up @@ -116,13 +110,6 @@ export const TokensPageView: FC<
</span>
</TableCell>

{viewAllTokens && (
<TableCell>
<span style={{ color: theme.palette.text.secondary }}>
{token.username}
</span>
</TableCell>
)}
<TableCell>
<span style={{ color: theme.palette.text.secondary }}>
<IconButton
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export { ConfirmDeleteDialog } from "./ConfirmDeleteDialog"
export { TokensSwitch } from "./TokensSwitch"
24 changes: 1 addition & 23 deletions site/src/pages/UserSettingsPage/TokensPage/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,9 @@ import {
useQueryClient,
QueryKey,
} from "@tanstack/react-query"
import { getTokens, deleteToken, checkAuthorization } from "api/api"
import { getTokens, deleteToken } from "api/api"
import { TokensFilter } from "api/typesGenerated"

// Owners have the ability to read all API tokens,
// whereas members can only see the tokens they have created.
// We check permissions here to determine whether to display the
// 'View All' switch on the TokensPage.
export const useCheckTokenPermissions = () => {
const queryKey = ["auth"]
const params = {
checks: {
readAllApiKeys: {
object: {
resource_type: "api_key",
},
action: "read",
},
},
}
return useQuery({
queryKey,
queryFn: () => checkAuthorization(params),
})
}

// Load all tokens
export const useTokensData = ({ include_all }: TokensFilter) => {
const queryKey = ["tokens", include_all]
Expand Down
3 changes: 2 additions & 1 deletion site/src/util/formUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,11 @@ export const getFormHelpers =
}

export const onChangeTrimmed =
<T>(form: FormikContextType<T>) =>
<T>(form: FormikContextType<T>, callback?: () => void) =>
(event: ChangeEvent<HTMLInputElement>): void => {
event.target.value = event.target.value.trim()
form.handleChange(event)
callback && callback()
}

// REMARK: Keep these consts in sync with coderd/httpapi/httpapi.go
Expand Down
0