Skip to content

Commit

Permalink
Implement Duplicate Functionality for API Keys (#3395)
Browse files Browse the repository at this point in the history
* add duplicate functionality

* sort by name

* clear create from ID on close
  • Loading branch information
mastercactapus authored Nov 1, 2023
1 parent d790c98 commit 8919e6c
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 23 deletions.
68 changes: 53 additions & 15 deletions web/src/app/admin/AdminAPIKeys.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,11 @@ const useStyles = makeStyles((theme: Theme) => ({
export default function AdminAPIKeys(): JSX.Element {
const classes = useStyles()
const [selectedAPIKey, setSelectedAPIKey] = useState<GQLAPIKey | null>(null)
const [createAPIKeyDialogClose, onCreateAPIKeyDialogClose] = useState(false)
const [createDialog, setCreateDialog] = useState<boolean>(false)
const [createFromID, setCreateFromID] = useState('')
const [editDialog, setEditDialog] = useState<string | undefined>()
const [deleteDialog, setDeleteDialog] = useState<string | undefined>()

// handles the openning of the create dialog form which is used for creating new API Key
const handleOpenCreateDialog = (): void => {
onCreateAPIKeyDialogClose(!createAPIKeyDialogClose)
}

// Get API Key triggers/actions
const [{ data, fetching, error }] = useQuery({ query })

Expand All @@ -72,7 +68,36 @@ export default function AdminAPIKeys(): JSX.Element {
return <Spinner />
}

const items = data.gqlAPIKeys.map(
const sortedByName = data.gqlAPIKeys.sort((a: GQLAPIKey, b: GQLAPIKey) => {
// We want to sort by name, but handle numbers in the name, in addition to text, so we'll break them out
// into words and sort by each "word".

// Split the name into words
const aWords = a.name.split(' ')
const bWords = b.name.split(' ')

// Loop through each word
for (let i = 0; i < aWords.length; i++) {
// If the word doesn't exist in the other name, it should be sorted first
if (!bWords[i]) {
return 1
}

// If the word is a number, convert it to a number
const aWord = isNaN(Number(aWords[i])) ? aWords[i] : Number(aWords[i])
const bWord = isNaN(Number(bWords[i])) ? bWords[i] : Number(bWords[i])

// If the words are not equal, return the comparison
if (aWord !== bWord) {
return aWord > bWord ? 1 : -1
}
}

// If we've made it this far, the words are equal, so return 0
return 0
})

const items = sortedByName.map(
(key: GQLAPIKey): FlatListListItem => ({
selected: (key as GQLAPIKey).id === selectedAPIKey?.id,
highlight: (key as GQLAPIKey).id === selectedAPIKey?.id,
Expand Down Expand Up @@ -123,6 +148,13 @@ export default function AdminAPIKeys(): JSX.Element {
label: 'Delete',
onClick: () => setDeleteDialog(key.id),
},
{
label: 'Duplicate',
onClick: () => {
setCreateDialog(true)
setCreateFromID(key.id)
},
},
]}
/>
</Grid>
Expand All @@ -139,28 +171,34 @@ export default function AdminAPIKeys(): JSX.Element {
setSelectedAPIKey(null)
}}
apiKeyID={selectedAPIKey?.id}
onDuplicateClick={() => {
setCreateDialog(true)
setCreateFromID(selectedAPIKey?.id || '')
}}
/>
{createAPIKeyDialogClose ? (
{createDialog && (
<AdminAPIKeyCreateDialog
fromID={createFromID}
onClose={() => {
onCreateAPIKeyDialogClose(false)
setCreateDialog(false)
setCreateFromID('')
}}
/>
) : null}
{deleteDialog ? (
)}
{deleteDialog && (
<AdminAPIKeyDeleteDialog
onClose={(): void => {
setDeleteDialog('')
}}
apiKeyID={deleteDialog}
/>
) : null}
{editDialog ? (
)}
{editDialog && (
<AdminAPIKeyEditDialog
onClose={() => setEditDialog('')}
apiKeyID={editDialog}
/>
) : null}
)}
<div
className={
selectedAPIKey ? classes.containerSelected : classes.containerDefault
Expand All @@ -171,7 +209,7 @@ export default function AdminAPIKeys(): JSX.Element {
data-cy='new'
variant='contained'
className={classes.buttons}
onClick={handleOpenCreateDialog}
onClick={() => setCreateDialog(true)}
startIcon={<Add />}
>
Create API Key
Expand Down
53 changes: 50 additions & 3 deletions web/src/app/admin/admin-api-keys/AdminAPIKeyCreateDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React, { useState } from 'react'
import { gql, useMutation } from 'urql'
import React, { useEffect, useState } from 'react'
import { gql, useMutation, useQuery } from 'urql'
import CopyText from '../../util/CopyText'
import { fieldErrors, nonFieldErrors } from '../../util/errutil'
import FormDialog from '../../dialogs/FormDialog'
import AdminAPIKeyForm from './AdminAPIKeyForm'
import { CreateGQLAPIKeyInput } from '../../../schema'
import { CreateGQLAPIKeyInput, GQLAPIKey } from '../../../schema'
import { CheckCircleOutline as SuccessIcon } from '@mui/icons-material'
import { DateTime } from 'luxon'
import { Grid, Typography, FormHelperText } from '@mui/material'
Expand All @@ -20,6 +20,20 @@ const newGQLAPIKeyQuery = gql`
}
`

const fromExistingQuery = gql`
query {
gqlAPIKeys {
id
name
description
role
allowedFields
createdAt
expiresAt
}
}
`

function AdminAPIKeyToken(props: { token: string }): React.ReactNode {
return (
<Grid item xs={12}>
Expand All @@ -34,8 +48,18 @@ function AdminAPIKeyToken(props: { token: string }): React.ReactNode {
)
}

// nextName will increment the number (if any) at the end of the name.
function nextName(name: string): string {
const match = name.match(/^(.*?)\s*(\d+)?$/)
if (!match) return name
const [, base, num] = match
if (!num) return `${base} 2`
return `${base} ${parseInt(num) + 1}`
}

export default function AdminAPIKeyCreateDialog(props: {
onClose: () => void
fromID?: string
}): React.ReactNode {
const [value, setValue] = useState<CreateGQLAPIKeyInput>({
name: '',
Expand All @@ -46,6 +70,29 @@ export default function AdminAPIKeyCreateDialog(props: {
})
const [status, createKey] = useMutation(newGQLAPIKeyQuery)
const token = status.data?.createGQLAPIKey?.token || null
const [{ data }] = useQuery({
query: fromExistingQuery,
pause: !props.fromID,
})

useEffect(() => {
if (!data?.gqlAPIKeys?.length) return
const from = data.gqlAPIKeys.find((k: GQLAPIKey) => k.id === props.fromID)
if (!from) return

const created = DateTime.fromISO(from.createdAt)
const expires = DateTime.fromISO(from.expiresAt)

const keyLifespan = expires.diff(created, 'days').days

setValue({
name: nextName(from.name),
description: from.description,
allowedFields: from.allowedFields,
expiresAt: DateTime.utc().plus({ days: keyLifespan }).toISO(),
role: from.role,
})
}, [data?.gqlAPIKeys])

// handles form on submit event, based on the action type (edit, create) it will send the necessary type of parameter
// token is also being set here when create action is used
Expand Down
13 changes: 8 additions & 5 deletions web/src/app/admin/admin-api-keys/AdminAPIKeyDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const query = gql`
interface Props {
onClose: () => void
apiKeyID?: string
onDuplicateClick: () => void
}

const useStyles = makeStyles(() => ({
Expand Down Expand Up @@ -166,11 +167,13 @@ export default function AdminAPIKeyDrawer(props: Props): JSX.Element {
</List>
<Grid className={classes.buttons}>
<ButtonGroup variant='contained'>
<Button data-cy='delete' onClick={() => setDialogDialog(true)}>
Delete
</Button>
<Button data-cy='edit' onClick={() => setEditDialog(true)}>
Edit
<Button onClick={() => setDialogDialog(true)}>Delete</Button>
<Button onClick={() => setEditDialog(true)}>Edit</Button>
<Button
onClick={() => props.onDuplicateClick()}
title='Create a new API Key with the same settings as this one.'
>
Duplicate
</Button>
</ButtonGroup>
</Grid>
Expand Down

0 comments on commit 8919e6c

Please sign in to comment.