Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,29 @@ const ModuleIssueDetailsPage = () => {
const isOverdue = deadlineUTC < todayUTC
const daysLeft = Math.ceil((deadlineUTC.getTime() - todayUTC.getTime()) / (1000 * 60 * 60 * 24))

const statusText = isOverdue
? '(overdue)'
: daysLeft === 0
? '(today)'
: `(${daysLeft} days left)`
let statusText: string
if (isOverdue) {
statusText = '(overdue)'
} else if (daysLeft === 0) {
statusText = '(today)'
} else {
statusText = `(${daysLeft} days left)`
}

const displayDate = deadlineDate.toLocaleDateString()

let color: string
if (isOverdue) {
color = 'text-[#DA3633]'
} else if (daysLeft <= 3) {
color = 'text-[#F59E0B]'
} else {
color = 'text-gray-600 dark:text-gray-300'
}

return {
text: `${displayDate} ${statusText}`,
color: isOverdue
? 'text-[#DA3633]'
: daysLeft <= 3
? 'text-[#F59E0B]'
: 'text-gray-600 dark:text-gray-300',
color,
}
}
const { data, loading, error } = useQuery(GetModuleIssueViewDocument, {
Expand Down Expand Up @@ -101,6 +109,47 @@ const ModuleIssueDetailsPage = () => {
const remainingLabels = labels.length - visibleLabels.length
const canEditDeadline = assignees.length > 0

let issueStatusClass: string
let issueStatusLabel: string
if (issue.state === 'open') {
issueStatusClass = 'bg-[#238636] text-white'
issueStatusLabel = 'Open'
} else if (issue.isMerged) {
issueStatusClass = 'bg-[#8657E5] text-white'
issueStatusLabel = 'Merged'
} else {
issueStatusClass = 'bg-[#DA3633] text-white'
issueStatusLabel = 'Closed'
}

const getPRStatus = (pr: Exclude<typeof issue.pullRequests, undefined>[0]) => {
let backgroundColor: string
let label: string
if (pr.state === 'closed' && pr.mergedAt) {
backgroundColor = '#8657E5'
label = 'Merged'
} else if (pr.state === 'closed') {
backgroundColor = '#DA3633'
label = 'Closed'
} else {
backgroundColor = '#238636'
label = 'Open'
}
return { backgroundColor, label }
}

const getAssignButtonTitle = (assigning: boolean) => {
let title: string
if (!issueId) {
title = 'Loading issue…'
} else if (assigning) {
title = 'Assigning…'
} else {
title = 'Assign to this user'
}
return title
}

return (
<div className="min-h-screen bg-white p-8 text-gray-700 dark:bg-[#212529] dark:text-gray-300">
<div className="mx-auto max-w-5xl">
Expand All @@ -114,15 +163,9 @@ const ModuleIssueDetailsPage = () => {
{issue.organizationName}/{issue.repositoryName} • #{issue.number}
</span>
<span
className={`inline-flex items-center rounded-lg px-2 py-0.5 text-xs font-medium ${
issue.state === 'open'
? 'bg-[#238636] text-white'
: issue.isMerged
? 'bg-[#8657E5] text-white'
: 'bg-[#DA3633] text-white'
}`}
className={`inline-flex items-center rounded-lg px-2 py-0.5 text-xs font-medium ${issueStatusClass}`}
>
{issue.state === 'open' ? 'Open' : issue.isMerged ? 'Merged' : 'Closed'}
{issueStatusLabel}
</span>
</div>
</div>
Expand Down Expand Up @@ -340,28 +383,17 @@ const ModuleIssueDetailsPage = () => {
</div>
</div>
<div className="flex items-center gap-2">
{pr.state === 'closed' && pr.mergedAt ? (
<span
className="inline-flex items-center rounded-lg px-2 py-0.5 text-xs font-medium text-white"
style={{ backgroundColor: '#8657E5' }}
>
Merged
</span>
) : pr.state === 'closed' ? (
<span
className="inline-flex items-center rounded-lg px-2 py-0.5 text-xs font-medium text-white"
style={{ backgroundColor: '#DA3633' }}
>
Closed
</span>
) : (
<span
className="inline-flex items-center rounded-lg px-2 py-0.5 text-xs font-medium text-white"
style={{ backgroundColor: '#238636' }}
>
Open
</span>
)}
{(() => {
const { backgroundColor, label } = getPRStatus(pr)
return (
<span
className="inline-flex items-center rounded-lg px-2 py-0.5 text-xs font-medium text-white"
style={{ backgroundColor }}
>
{label}
</span>
)
})()}
</div>
</div>
))
Expand Down Expand Up @@ -415,9 +447,7 @@ const ModuleIssueDetailsPage = () => {
})
}}
className={`${getButtonClassName(!issueId || assigning)} px-3 py-1`}
title={
!issueId ? 'Loading issue…' : assigning ? 'Assigning…' : 'Assign to this user'
}
title={getAssignButtonTitle(assigning)}
>
<FaPlus className="text-gray-500" />
<span>Assign</span>
Expand Down
139 changes: 86 additions & 53 deletions frontend/src/app/settings/api-keys/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from '@herou
import { Input } from '@heroui/react'
import { addToast } from '@heroui/toast'
import { format, addDays } from 'date-fns'
import { useState } from 'react'
import React, { useState } from 'react'
import { FaInfoCircle } from 'react-icons/fa'
import { FaSpinner, FaKey, FaPlus, FaCopy, FaEye, FaEyeSlash, FaTrash } from 'react-icons/fa6'
import {
Expand All @@ -14,11 +14,80 @@ import {
RevokeApiKeyDocument,
} from 'types/__generated__/apiKeyQueries.generated'
import type { ApiKey } from 'types/apiKey'
import LoadingSpinner from 'components/LoadingSpinner'
import SecondaryCard from 'components/SecondaryCard'
import { ApiKeysSkeleton } from 'components/skeletons/ApiKeySkelton'

const MAX_ACTIVE_KEYS = 3

// Content state components
const ErrorState = () => (
<div className="rounded-md bg-red-50 p-4 text-red-700 dark:bg-red-900/20 dark:text-red-400">
Error loading API keys
</div>
)

const EmptyState = () => (
<div className="rounded-md bg-gray-50 p-8 text-center text-gray-500 dark:bg-gray-800/50 dark:text-gray-400">
You don't have any API keys yet.
</div>
)

interface ApiKeysTableProps {
data: { apiKeys?: ApiKey[] } | undefined
onRevoke: (key: ApiKey) => void
}

const ApiKeysTable = ({ data, onRevoke }: ApiKeysTableProps) => (
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr className="border-b-1 border-b-gray-200 dark:border-b-gray-700">
<th className="py-3 text-left font-semibold">Name</th>
<th className="py-3 text-left font-semibold">ID</th>
<th className="py-3 text-left font-semibold">Created</th>
<th className="py-3 text-left font-semibold">Expires</th>
<th className="py-3 text-right font-semibold">Actions</th>
</tr>
</thead>
<tbody>
{(data?.apiKeys ?? []).map((key: ApiKey) => (
<tr key={key.uuid} className="border-b border-b-gray-200 dark:border-b-gray-700">
<td className="py-3">{key.name}</td>
<td className="py-3 font-mono text-sm">{key.uuid}</td>
<td className="py-3">{format(new Date(key.createdAt), 'PP')}</td>
<td className="py-3">
{key.expiresAt ? format(new Date(key.expiresAt), 'PP') : 'Never'}
</td>
<td className="py-3 text-right">
<Button
variant="light"
size="sm"
onPress={() => onRevoke(key)}
className="text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20"
>
<FaTrash />
</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)

type ContentType = 'error' | 'loading' | 'empty' | 'table'

const getContentComponents = (
data: { apiKeys?: ApiKey[] } | undefined,
onRevoke: (key: ApiKey) => void
): Record<ContentType, () => React.ReactNode> => ({
error: () => <ErrorState />,
loading: () => <LoadingSpinner />,
empty: () => <EmptyState />,
table: () => <ApiKeysTable data={data} onRevoke={onRevoke} />,
})

export default function Page() {
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false)
const [newKeyName, setNewKeyName] = useState('')
Expand Down Expand Up @@ -70,6 +139,21 @@ export default function Page() {
const canCreateNewKey = activeKeyCount < MAX_ACTIVE_KEYS
const defaultExpiryDate = format(addDays(new Date(), 30), 'yyyy-MM-dd')

const getContentType = (): ContentType => {
if (error) {
return 'error'
} else if (loading) {
return 'loading'
} else if (data?.apiKeys?.length === 0) {
return 'empty'
} else {
return 'table'
}
}

const contentType = getContentType()
const contentComponents = getContentComponents(data, setKeyToRevoke)

const handleCreateKey = () => {
if (!newKeyName.trim()) {
addToast({ title: 'Error', description: 'Please provide a name', color: 'danger' })
Expand Down Expand Up @@ -202,58 +286,7 @@ export default function Page() {
</Button>
</div>

{error ? (
<div className="rounded-md bg-red-50 p-4 text-red-700 dark:bg-red-900/20 dark:text-red-400">
Error loading API keys
</div>
) : loading ? (
<div className="flex flex-col gap-4">
<ApiKeysSkeleton />
</div>
) : !data?.apiKeys?.length ? (
<div className="rounded-md bg-gray-50 p-8 text-center text-gray-500 dark:bg-gray-800/50 dark:text-gray-400">
You don't have any API keys yet.
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr className="border-b-1 border-b-gray-200 dark:border-b-gray-700">
<th className="py-3 text-left font-semibold">Name</th>
<th className="py-3 text-left font-semibold">ID</th>
<th className="py-3 text-left font-semibold">Created</th>
<th className="py-3 text-left font-semibold">Expires</th>
<th className="py-3 text-right font-semibold">Actions</th>
</tr>
</thead>
<tbody>
{data.apiKeys.map((key: ApiKey) => (
<tr
key={key.uuid}
className="border-b border-b-gray-200 dark:border-b-gray-700"
>
<td className="py-3">{key.name}</td>
<td className="py-3 font-mono text-sm">{key.uuid}</td>
<td className="py-3">{format(new Date(key.createdAt), 'PP')}</td>
<td className="py-3">
{key.expiresAt ? format(new Date(key.expiresAt), 'PP') : 'Never'}
</td>
<td className="py-3 text-right">
<Button
variant="light"
size="sm"
onPress={() => setKeyToRevoke(key)}
className="text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20"
>
<FaTrash />
</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{contentComponents[contentType]()}
</SecondaryCard>

<SecondaryCard>
Expand Down