Merge pull request #1674 from omnivore-app/fix/api-page-table
Use the same SettingsTable for the API keys
This commit is contained in:
@ -92,6 +92,7 @@ export function GeneralFormInput(props: FormInputProps): JSX.Element {
|
||||
return (
|
||||
<select
|
||||
onChange={input.onChange}
|
||||
defaultValue={props.value}
|
||||
style={{
|
||||
padding: '8px',
|
||||
height: '38px',
|
||||
|
||||
@ -6,14 +6,13 @@ import {
|
||||
import { VStack, HStack } from '../elements/LayoutPrimitives'
|
||||
import { Button } from '../elements/Button'
|
||||
import { StyledText } from '../elements/StyledText'
|
||||
import { useConfirmListener } from '../../lib/keyboardShortcuts/useKeyboardShortcuts'
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
type ConfirmationModalProps = {
|
||||
message?: string
|
||||
richMessage?: React.ReactNode
|
||||
icon?: React.ReactNode
|
||||
acceptButtonLabel?: string
|
||||
cancelButtonLabel?: string
|
||||
onAccept: () => void
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
@ -42,7 +41,7 @@ export function ConfirmationModal(props: ConfirmationModalProps): JSX.Element {
|
||||
}
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
{props.cancelButtonLabel ? props.cancelButtonLabel : 'Cancel'}
|
||||
</Button>
|
||||
<Button
|
||||
style="ctaDarkYellow"
|
||||
|
||||
@ -5,13 +5,10 @@ import {
|
||||
ModalRoot,
|
||||
ModalTitleBar,
|
||||
} from '../elements/ModalPrimitives'
|
||||
import { Box, HStack, VStack } from '../elements/LayoutPrimitives'
|
||||
import { Button } from '../elements/Button'
|
||||
import { Box, VStack } from '../elements/LayoutPrimitives'
|
||||
import { StyledText } from '../elements/StyledText'
|
||||
import { useState } from 'react'
|
||||
import { FormInputProps, GeneralFormInput } from '../elements/FormElements'
|
||||
import { theme } from '../tokens/stitches.config'
|
||||
import { X } from 'phosphor-react'
|
||||
|
||||
export interface FormModalProps {
|
||||
inputs?: FormInputProps[]
|
||||
@ -35,7 +32,10 @@ export function FormModal(props: FormModalProps): JSX.Element {
|
||||
css={{ overflow: 'auto', px: '24px' }}
|
||||
>
|
||||
<VStack>
|
||||
<ModalTitleBar title={props.title} onOpenChange={props.onOpenChange} />
|
||||
<ModalTitleBar
|
||||
title={props.title}
|
||||
onOpenChange={props.onOpenChange}
|
||||
/>
|
||||
<Box css={{ width: '100%' }}>
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
@ -46,15 +46,21 @@ export function FormModal(props: FormModalProps): JSX.Element {
|
||||
>
|
||||
{inputs.map((input, index) => (
|
||||
<VStack key={index}>
|
||||
<StyledText style={'menuTitle'} css={{ pt: index > 0 ? '10px' : 'unset' }}>
|
||||
<StyledText
|
||||
style={'menuTitle'}
|
||||
css={{ pt: index > 0 ? '10px' : 'unset' }}
|
||||
>
|
||||
{input.label}
|
||||
</StyledText>
|
||||
</StyledText>
|
||||
<Box css={{ width: '100%' }}>
|
||||
<GeneralFormInput {...input} />
|
||||
</Box>
|
||||
</VStack>
|
||||
))}
|
||||
<ModalButtonBar onOpenChange={props.onOpenChange} acceptButtonLabel={props.acceptButtonLabel} />
|
||||
<ModalButtonBar
|
||||
onOpenChange={props.onOpenChange}
|
||||
acceptButtonLabel={props.acceptButtonLabel}
|
||||
/>
|
||||
</form>
|
||||
</Box>
|
||||
</VStack>
|
||||
|
||||
@ -28,7 +28,6 @@ type CreateButtonProps = {
|
||||
}
|
||||
|
||||
type SettingsTableRowProps = {
|
||||
key: string
|
||||
title: string
|
||||
isLast: boolean
|
||||
|
||||
@ -66,12 +65,12 @@ const MoreOptions = (props: MoreOptionsProps) => (
|
||||
>
|
||||
<DropdownOption
|
||||
onSelect={() => {
|
||||
return true
|
||||
props.onDelete()
|
||||
}}
|
||||
>
|
||||
<HStack alignment={'center'} distribution={'start'}>
|
||||
<Trash size={24} color={theme.colors.omnivoreRed.toString()} />
|
||||
<Button
|
||||
<SpanBox
|
||||
css={{
|
||||
color: theme.colors.omnivoreRed.toString(),
|
||||
marginLeft: '8px',
|
||||
@ -82,10 +81,9 @@ const MoreOptions = (props: MoreOptionsProps) => (
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
}}
|
||||
onClick={props.onDelete}
|
||||
>
|
||||
{props.title}
|
||||
</Button>
|
||||
</SpanBox>
|
||||
</HStack>
|
||||
</DropdownOption>
|
||||
</Dropdown>
|
||||
@ -121,7 +119,6 @@ export const EmptySettingsRow = (props: EmptySettingsRowProps): JSX.Element => {
|
||||
export const SettingsTableRow = (props: SettingsTableRowProps): JSX.Element => {
|
||||
return (
|
||||
<Box
|
||||
key={props.key}
|
||||
css={{
|
||||
backgroundColor: '$grayBg',
|
||||
display: 'flex',
|
||||
|
||||
@ -7,9 +7,9 @@ export interface ApiKey {
|
||||
name: string
|
||||
key?: string
|
||||
scopes: string[]
|
||||
createdAt: Date
|
||||
expiresAt: Date
|
||||
usedAt?: Date
|
||||
createdAt: string
|
||||
expiresAt: string
|
||||
usedAt?: string
|
||||
}
|
||||
|
||||
interface ApiKeysQueryResponse {
|
||||
@ -66,7 +66,7 @@ export function useGetApiKeysQuery(): ApiKeysQueryResponse {
|
||||
console.log('error', error)
|
||||
}
|
||||
return {
|
||||
isValidating: false,
|
||||
isValidating: true,
|
||||
apiKeys: [],
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
revalidate: () => {},
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
|
||||
import { showErrorToast, showSuccessToast } from '../../lib/toastHelpers'
|
||||
import { applyStoredTheme } from '../../lib/themeUpdater'
|
||||
@ -8,64 +7,28 @@ import { useGetApiKeysQuery } from '../../lib/networking/queries/useGetApiKeysQu
|
||||
import { generateApiKeyMutation } from '../../lib/networking/mutations/generateApiKeyMutation'
|
||||
import { revokeApiKeyMutation } from '../../lib/networking/mutations/revokeApiKeyMutation'
|
||||
|
||||
import { PrimaryLayout } from '../../components/templates/PrimaryLayout'
|
||||
import { Table } from '../../components/elements/Table'
|
||||
|
||||
import { FormInputProps } from '../../components/elements/FormElements'
|
||||
import { FormModal } from '../../components/patterns/FormModal'
|
||||
import { ConfirmationModal } from '../../components/patterns/ConfirmationModal'
|
||||
|
||||
interface ApiKey {
|
||||
name: string
|
||||
scopes: string
|
||||
expiresAt: string
|
||||
usedAt: string
|
||||
}
|
||||
import {
|
||||
EmptySettingsRow,
|
||||
SettingsTable,
|
||||
SettingsTableRow,
|
||||
} from '../../components/templates/settings/SettingsTable'
|
||||
import { StyledText } from '../../components/elements/StyledText'
|
||||
import { formattedShortDate } from '../../lib/dateFormatting'
|
||||
|
||||
export default function Api(): JSX.Element {
|
||||
const { apiKeys, revalidate } = useGetApiKeysQuery()
|
||||
const { apiKeys, revalidate, isValidating } = useGetApiKeysQuery()
|
||||
const [onDeleteId, setOnDeleteId] = useState<string>('')
|
||||
const [addModalOpen, setAddModalOpen] = useState(false)
|
||||
const [name, setName] = useState<string>('')
|
||||
const [value, setValue] = useState<string>('')
|
||||
// const [scopes, setScopes] = useState<string[] | undefined>(undefined)
|
||||
const [expiresAt, setExpiresAt] = useState<Date>(new Date())
|
||||
const [formInputs, setFormInputs] = useState<FormInputProps[]>([])
|
||||
const [apiKeyGenerated, setApiKeyGenerated] = useState('')
|
||||
// default expiry date is 1 year from now
|
||||
const defaultExpiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 365)
|
||||
.toISOString()
|
||||
.split('T')[0]
|
||||
const neverExpiresDate = new Date(8640000000000000)
|
||||
|
||||
const router = useRouter()
|
||||
useEffect(() => {
|
||||
if (Object.keys(router.query).length) {
|
||||
setValue(`${router.query?.create}`)
|
||||
setExpiresAt(new Date(defaultExpiresAt))
|
||||
onAdd()
|
||||
setAddModalOpen(true)
|
||||
}
|
||||
}, [router.query])
|
||||
|
||||
const headers = ['Name', 'Scopes', 'Used at', 'Expires on']
|
||||
const rows = useMemo(() => {
|
||||
const rows = new Map<string, ApiKey>()
|
||||
apiKeys.forEach((apiKey) =>
|
||||
rows.set(apiKey.id, {
|
||||
name: apiKey.name,
|
||||
scopes: apiKey.scopes.join(', ') || 'All',
|
||||
usedAt: apiKey.usedAt
|
||||
? new Date(apiKey.usedAt).toISOString()
|
||||
: 'Never used',
|
||||
expiresAt:
|
||||
new Date(apiKey.expiresAt).getTime() != neverExpiresDate.getTime()
|
||||
? new Date(apiKey.expiresAt).toDateString()
|
||||
: 'Never',
|
||||
})
|
||||
)
|
||||
return rows
|
||||
}, [apiKeys])
|
||||
const defaultExpiresAt = 'Never'
|
||||
|
||||
applyStoredTheme(false)
|
||||
|
||||
@ -96,7 +59,7 @@ export default function Api(): JSX.Element {
|
||||
label: 'Name',
|
||||
onChange: setName,
|
||||
name: 'name',
|
||||
value: `${router.query?.create ? router.query?.create : value}`,
|
||||
value: value,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
@ -104,6 +67,7 @@ export default function Api(): JSX.Element {
|
||||
name: 'expiredAt',
|
||||
required: true,
|
||||
onChange: (e) => {
|
||||
console.log('onChange: ', e)
|
||||
let additionalDays = 0
|
||||
switch (e.target.value) {
|
||||
case 'in 7 days':
|
||||
@ -140,13 +104,65 @@ export default function Api(): JSX.Element {
|
||||
])
|
||||
}
|
||||
|
||||
const sortedApiKeys = useMemo(() => {
|
||||
if (!apiKeys) {
|
||||
return []
|
||||
}
|
||||
return apiKeys.sort((a, b) => a.createdAt.localeCompare(b.createdAt))
|
||||
}, [apiKeys])
|
||||
|
||||
return (
|
||||
<PrimaryLayout pageTestId={'api-keys'}>
|
||||
<Toaster
|
||||
containerStyle={{
|
||||
top: '5rem',
|
||||
}}
|
||||
/>
|
||||
<SettingsTable
|
||||
pageId="api-keys"
|
||||
pageHeadline="API Keys"
|
||||
pageInfoLink="https://docs.omnivore.app/integrations/api.html"
|
||||
headerTitle="API Keys"
|
||||
createTitle="Generate API Key"
|
||||
createAction={() => {
|
||||
onAdd()
|
||||
setName('')
|
||||
setExpiresAt(neverExpiresDate)
|
||||
setAddModalOpen(true)
|
||||
}}
|
||||
>
|
||||
{sortedApiKeys.length > 0 ? (
|
||||
sortedApiKeys.map((apiKey, i) => {
|
||||
return (
|
||||
<SettingsTableRow
|
||||
key={apiKey.id}
|
||||
title={apiKey.name}
|
||||
isLast={i === apiKeys.length - 1}
|
||||
onDelete={() => {
|
||||
console.log('onDelete triggered: ', apiKey.id)
|
||||
setOnDeleteId(apiKey.id)
|
||||
}}
|
||||
deleteTitle="Delete"
|
||||
sublineElement={
|
||||
<StyledText
|
||||
css={{
|
||||
my: '5px',
|
||||
fontSize: '11px',
|
||||
a: {
|
||||
color: '$omnivoreCtaYellow',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{`Last used: ${
|
||||
apiKey.usedAt ? formattedShortDate(apiKey.usedAt) : 'Never'
|
||||
}, `}
|
||||
{`Created: ${formattedShortDate(apiKey.createdAt)}, `}
|
||||
{apiKey.expiresAt &&
|
||||
apiKey.expiresAt != neverExpiresDate.toISOString()
|
||||
? `Expires: ${formattedShortDate(apiKey.expiresAt)}`
|
||||
: 'Never expires'}
|
||||
</StyledText>
|
||||
}
|
||||
/>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<EmptySettingsRow text={isValidating ? '-' : 'No API Keys Found'} />
|
||||
)}
|
||||
|
||||
{addModalOpen && (
|
||||
<FormModal
|
||||
@ -163,7 +179,8 @@ export default function Api(): JSX.Element {
|
||||
message={`API key generated. Copy the key and use it in your application.
|
||||
You won’t be able to see it again!
|
||||
Key: ${apiKeyGenerated}`}
|
||||
acceptButtonLabel={'Copy'}
|
||||
acceptButtonLabel="Copy"
|
||||
cancelButtonLabel="Close"
|
||||
onAccept={async () => {
|
||||
await navigator.clipboard.writeText(apiKeyGenerated)
|
||||
setApiKeyGenerated('')
|
||||
@ -174,7 +191,7 @@ export default function Api(): JSX.Element {
|
||||
|
||||
{onDeleteId && (
|
||||
<ConfirmationModal
|
||||
message={'API key would be revoked. This action cannot be undone.'}
|
||||
message={'API key will be revoked. This action cannot be undone.'}
|
||||
onAccept={async () => {
|
||||
await onDelete(onDeleteId)
|
||||
setOnDeleteId('')
|
||||
@ -182,19 +199,6 @@ export default function Api(): JSX.Element {
|
||||
onOpenChange={() => setOnDeleteId('')}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Table
|
||||
heading={'API Keys'}
|
||||
headers={headers}
|
||||
rows={rows}
|
||||
onDelete={setOnDeleteId}
|
||||
onAdd={() => {
|
||||
onAdd()
|
||||
setName('')
|
||||
setExpiresAt(new Date(defaultExpiresAt))
|
||||
setAddModalOpen(true)
|
||||
}}
|
||||
/>
|
||||
</PrimaryLayout>
|
||||
</SettingsTable>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user