From 14265feeba3bd5f382861012f372b33409e349af Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Mon, 30 Oct 2023 17:35:40 -0500 Subject: [PATCH 1/3] add duplicate functionality --- web/src/app/admin/AdminAPIKeys.tsx | 38 +++++++------ .../AdminAPIKeyCreateDialog.tsx | 53 +++++++++++++++++-- .../admin-api-keys/AdminAPIKeyDrawer.tsx | 13 +++-- 3 files changed, 80 insertions(+), 24 deletions(-) diff --git a/web/src/app/admin/AdminAPIKeys.tsx b/web/src/app/admin/AdminAPIKeys.tsx index e99b32b8c7..3c83484ef1 100644 --- a/web/src/app/admin/AdminAPIKeys.tsx +++ b/web/src/app/admin/AdminAPIKeys.tsx @@ -52,15 +52,11 @@ const useStyles = makeStyles((theme: Theme) => ({ export default function AdminAPIKeys(): JSX.Element { const classes = useStyles() const [selectedAPIKey, setSelectedAPIKey] = useState(null) - const [createAPIKeyDialogClose, onCreateAPIKeyDialogClose] = useState(false) + const [createDialog, setCreateDialog] = useState(false) + const [createFromID, setCreateFromID] = useState('') const [editDialog, setEditDialog] = useState() const [deleteDialog, setDeleteDialog] = useState() - // 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 }) @@ -123,6 +119,13 @@ export default function AdminAPIKeys(): JSX.Element { label: 'Delete', onClick: () => setDeleteDialog(key.id), }, + { + label: 'Duplicate', + onClick: () => { + setCreateDialog(true) + setCreateFromID(key.id) + }, + }, ]} /> @@ -139,28 +142,31 @@ export default function AdminAPIKeys(): JSX.Element { setSelectedAPIKey(null) }} apiKeyID={selectedAPIKey?.id} + onDuplicateClick={() => { + setCreateDialog(true) + setCreateFromID(selectedAPIKey?.id || '') + }} /> - {createAPIKeyDialogClose ? ( + {createDialog && ( { - onCreateAPIKeyDialogClose(false) - }} + fromID={createFromID} + onClose={() => setCreateDialog(false)} /> - ) : null} - {deleteDialog ? ( + )} + {deleteDialog && ( { setDeleteDialog('') }} apiKeyID={deleteDialog} /> - ) : null} - {editDialog ? ( + )} + {editDialog && ( setEditDialog('')} apiKeyID={editDialog} /> - ) : null} + )}
setCreateDialog(true)} startIcon={} > Create API Key diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeyCreateDialog.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeyCreateDialog.tsx index 85ac79f9ed..d812c3ceb3 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeyCreateDialog.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeyCreateDialog.tsx @@ -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' @@ -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 ( @@ -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({ name: '', @@ -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 diff --git a/web/src/app/admin/admin-api-keys/AdminAPIKeyDrawer.tsx b/web/src/app/admin/admin-api-keys/AdminAPIKeyDrawer.tsx index 02a6f01afc..c7ba012222 100644 --- a/web/src/app/admin/admin-api-keys/AdminAPIKeyDrawer.tsx +++ b/web/src/app/admin/admin-api-keys/AdminAPIKeyDrawer.tsx @@ -54,6 +54,7 @@ const query = gql` interface Props { onClose: () => void apiKeyID?: string + onDuplicateClick: () => void } const useStyles = makeStyles(() => ({ @@ -166,11 +167,13 @@ export default function AdminAPIKeyDrawer(props: Props): JSX.Element { - - + + From 4ba6b2cb1df06b54df918aea8a354e5b8265dca9 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Mon, 30 Oct 2023 17:41:38 -0500 Subject: [PATCH 2/3] sort by name --- web/src/app/admin/AdminAPIKeys.tsx | 31 +++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/web/src/app/admin/AdminAPIKeys.tsx b/web/src/app/admin/AdminAPIKeys.tsx index 3c83484ef1..366cc03ac7 100644 --- a/web/src/app/admin/AdminAPIKeys.tsx +++ b/web/src/app/admin/AdminAPIKeys.tsx @@ -68,7 +68,36 @@ export default function AdminAPIKeys(): JSX.Element { return } - 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, From ace2ed242a86274e73f732b54ccba6f7e2caf6f8 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Wed, 1 Nov 2023 09:42:40 -0500 Subject: [PATCH 3/3] clear create from ID on close --- web/src/app/admin/AdminAPIKeys.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/web/src/app/admin/AdminAPIKeys.tsx b/web/src/app/admin/AdminAPIKeys.tsx index 366cc03ac7..f7d8b7e1f6 100644 --- a/web/src/app/admin/AdminAPIKeys.tsx +++ b/web/src/app/admin/AdminAPIKeys.tsx @@ -179,7 +179,10 @@ export default function AdminAPIKeys(): JSX.Element { {createDialog && ( setCreateDialog(false)} + onClose={() => { + setCreateDialog(false) + setCreateFromID('') + }} /> )} {deleteDialog && (