Skip to content
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

Implement Duplicate Functionality for API Keys #3395

Merged
merged 3 commits into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
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