import './APIKeys.styles.scss'; import { Color } from '@signozhq/design-tokens'; import { Avatar, Button, Col, Collapse, Flex, Form, Input, Modal, Radio, Row, Select, Table, TableProps, Tooltip, Typography, } from 'antd'; import { NotificationInstance } from 'antd/es/notification/interface'; import { CollapseProps } from 'antd/lib'; import createAPIKeyApi from 'api/APIKeys/createAPIKey'; import deleteAPIKeyApi from 'api/APIKeys/deleteAPIKey'; import updateAPIKeyApi from 'api/APIKeys/updateAPIKey'; import axios, { AxiosError } from 'axios'; import cx from 'classnames'; import { SOMETHING_WENT_WRONG } from 'constants/api'; import dayjs from 'dayjs'; import { useGetAllAPIKeys } from 'hooks/APIKeys/useGetAllAPIKeys'; import { useNotifications } from 'hooks/useNotifications'; import { CalendarClock, Check, ClipboardEdit, Contact2, Copy, Eye, Minus, PenLine, Plus, Search, Trash2, View, X, } from 'lucide-react'; import { useAppContext } from 'providers/App/App'; import { ChangeEvent, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useMutation } from 'react-query'; import { useCopyToClipboard } from 'react-use'; import { APIKeyProps } from 'types/api/pat/types'; import { USER_ROLES } from 'types/roles'; export const showErrorNotification = ( notifications: NotificationInstance, err: Error, ): void => { notifications.error({ message: axios.isAxiosError(err) ? err.message : SOMETHING_WENT_WRONG, }); }; type ExpiryOption = { value: string; label: string; }; export const EXPIRATION_WITHIN_SEVEN_DAYS = 7; const API_KEY_EXPIRY_OPTIONS: ExpiryOption[] = [ { value: '1', label: '1 day' }, { value: '7', label: '1 week' }, { value: '30', label: '1 month' }, { value: '90', label: '3 months' }, { value: '365', label: '1 year' }, { value: '0', label: 'No Expiry' }, ]; export const isExpiredToken = (expiryTimestamp: number): boolean => { if (expiryTimestamp === 0) { return false; } const currentTime = dayjs(); const tokenExpiresAt = dayjs.unix(expiryTimestamp); return tokenExpiresAt.isBefore(currentTime); }; export const getDateDifference = ( createdTimestamp: number, expiryTimestamp: number, ): number => { const differenceInSeconds = Math.abs(expiryTimestamp - createdTimestamp); // Convert seconds to days return differenceInSeconds / (60 * 60 * 24); }; function APIKeys(): JSX.Element { const { user } = useAppContext(); const { notifications } = useNotifications(); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [showNewAPIKeyDetails, setShowNewAPIKeyDetails] = useState(false); const [, handleCopyToClipboard] = useCopyToClipboard(); const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [activeAPIKey, setActiveAPIKey] = useState(); const [searchValue, setSearchValue] = useState(''); const [dataSource, setDataSource] = useState([]); const { t } = useTranslation(['apiKeys']); const [editForm] = Form.useForm(); const [createForm] = Form.useForm(); const handleFormReset = (): void => { editForm.resetFields(); createForm.resetFields(); }; const hideDeleteViewModal = (): void => { handleFormReset(); setActiveAPIKey(null); setIsDeleteModalOpen(false); }; const showDeleteModal = (apiKey: APIKeyProps): void => { setActiveAPIKey(apiKey); setIsDeleteModalOpen(true); }; const hideEditViewModal = (): void => { handleFormReset(); setActiveAPIKey(null); setIsEditModalOpen(false); }; const hideAddViewModal = (): void => { handleFormReset(); setShowNewAPIKeyDetails(false); setActiveAPIKey(null); setIsAddModalOpen(false); }; const showEditModal = (apiKey: APIKeyProps): void => { handleFormReset(); setActiveAPIKey(apiKey); editForm.setFieldsValue({ name: apiKey.name, role: apiKey.role || USER_ROLES.VIEWER, }); setIsEditModalOpen(true); }; const showAddModal = (): void => { setActiveAPIKey(null); setIsAddModalOpen(true); }; const handleModalClose = (): void => { setActiveAPIKey(null); }; const { data: APIKeys, isLoading, isRefetching, refetch: refetchAPIKeys, error, isError, } = useGetAllAPIKeys(); useEffect(() => { setActiveAPIKey(APIKeys?.data.data[0]); }, [APIKeys]); useEffect(() => { setDataSource(APIKeys?.data.data || []); }, [APIKeys?.data.data]); useEffect(() => { if (isError) { showErrorNotification(notifications, error as AxiosError); } }, [error, isError, notifications]); const handleSearch = (e: ChangeEvent): void => { setSearchValue(e.target.value); const filteredData = APIKeys?.data?.data?.filter( (key: APIKeyProps) => key && key.name && key.name.toLowerCase().includes(e.target.value.toLowerCase()), ); setDataSource(filteredData || []); }; const clearSearch = (): void => { setSearchValue(''); }; const { mutate: createAPIKey, isLoading: isLoadingCreateAPIKey } = useMutation( createAPIKeyApi, { onSuccess: (data) => { setShowNewAPIKeyDetails(true); setActiveAPIKey(data.payload); refetchAPIKeys(); }, onError: (error) => { showErrorNotification(notifications, error as AxiosError); }, }, ); const { mutate: updateAPIKey, isLoading: isLoadingUpdateAPIKey } = useMutation( updateAPIKeyApi, { onSuccess: () => { refetchAPIKeys(); setIsEditModalOpen(false); }, onError: (error) => { showErrorNotification(notifications, error as AxiosError); }, }, ); const { mutate: deleteAPIKey, isLoading: isDeleteingAPIKey } = useMutation( deleteAPIKeyApi, { onSuccess: () => { refetchAPIKeys(); setIsDeleteModalOpen(false); }, onError: (error) => { showErrorNotification(notifications, error as AxiosError); }, }, ); const onDeleteHandler = (): void => { clearSearch(); if (activeAPIKey) { deleteAPIKey(activeAPIKey.id); } }; const onUpdateApiKey = (): void => { editForm .validateFields() .then((values) => { if (activeAPIKey) { updateAPIKey({ id: activeAPIKey.id, data: { name: values.name, role: values.role, }, }); } }) .catch((errorInfo) => { console.error('error info', errorInfo); }); }; const onCreateAPIKey = (): void => { createForm .validateFields() .then((values) => { if (user) { createAPIKey({ name: values.name, expiresInDays: parseInt(values.expiration, 10), role: values.role, }); } }) .catch((errorInfo) => { console.error('error info', errorInfo); }); }; const handleCopyKey = (text: string): void => { handleCopyToClipboard(text); notifications.success({ message: 'Copied to clipboard', }); }; const getFormattedTime = (epochTime: number): string => { const timeOptions: Intl.DateTimeFormatOptions = { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, }; const formattedTime = new Date(epochTime * 1000).toLocaleTimeString( 'en-US', timeOptions, ); const dateOptions: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', year: 'numeric', }; const formattedDate = new Date(epochTime * 1000).toLocaleDateString( 'en-US', dateOptions, ); return `${formattedDate} ${formattedTime}`; }; const handleCopyClose = (): void => { if (activeAPIKey) { handleCopyKey(activeAPIKey?.token); } hideAddViewModal(); }; const columns: TableProps['columns'] = [ { title: 'API Key', key: 'api-key', // eslint-disable-next-line sonarjs/cognitive-complexity render: (APIKey: APIKeyProps): JSX.Element => { const formattedDateAndTime = APIKey && APIKey?.lastUsed && APIKey?.lastUsed !== 0 ? getFormattedTime(APIKey?.lastUsed) : 'Never'; const createdOn = new Date(APIKey.createdAt).toLocaleString(); const expiresIn = APIKey.expiresAt === 0 ? Number.POSITIVE_INFINITY : getDateDifference( new Date(APIKey?.createdAt).getTime() / 1000, APIKey?.expiresAt, ); const isExpired = isExpiredToken(APIKey.expiresAt); const expiresOn = !APIKey.expiresAt || APIKey.expiresAt === 0 ? 'No Expiry' : getFormattedTime(APIKey.expiresAt); const updatedOn = !APIKey.updatedAt || APIKey.updatedAt === '' ? null : new Date(APIKey.updatedAt).toLocaleString(); const items: CollapseProps['items'] = [ { key: '1', label: (
{APIKey?.name}
{APIKey?.token.substring(0, 2)}******** {APIKey?.token.substring(APIKey.token.length - 2).trim()} { e.stopPropagation(); e.preventDefault(); handleCopyKey(APIKey.token); }} />
{APIKey.role === USER_ROLES.ADMIN && ( )} {APIKey.role === USER_ROLES.EDITOR && ( )} {APIKey.role === USER_ROLES.VIEWER && ( )} {!APIKey.role && ( )}
), children: (
{APIKey?.createdByUser && ( Creator {APIKey?.createdByUser?.name?.substring(0, 1)} {APIKey.createdByUser?.name}
{APIKey.createdByUser?.email}
)} Created on {createdOn} {updatedOn && ( Updated on {updatedOn} )} Expires on {expiresOn}
), }, ]; return (
Last used {formattedDateAndTime}
{!isExpired && expiresIn <= EXPIRATION_WITHIN_SEVEN_DAYS && (
Expires in {expiresIn} Days
)} {isExpired && (
Expired
)}
); }, }, ]; return (
API Keys Create and manage API keys for the SigNoz API
} value={searchValue} onChange={handleSearch} />
`${range[0]}-${range[1]} of ${total} keys`, }} /> {/* Delete Key Modal */} Delete Key} open={isDeleteModalOpen} closable afterClose={handleModalClose} onCancel={hideDeleteViewModal} destroyOnClose footer={[ , , ]} > {t('delete_confirm_message', { keyName: activeAPIKey?.name, })} {/* Edit Key Modal */} } > Cancel , , ]} >
Admin
Editor
Viewer
{/* Create New Key Modal */} } > Copy key and close , ] : [ , , ] } > {!showNewAPIKeyDetails && (
Admin
Editor
Viewer