import './IngestionSettings.styles.scss'; import { Color } from '@signozhq/design-tokens'; import { Button, Col, Collapse, DatePicker, Form, Input, InputNumber, Modal, Row, Select, Switch, Table, TablePaginationConfig, TableProps as AntDTableProps, Tag, Typography, } from 'antd'; import { NotificationInstance } from 'antd/es/notification/interface'; import { CollapseProps } from 'antd/lib'; import createIngestionKeyApi from 'api/IngestionKeys/createIngestionKey'; import deleteIngestionKey from 'api/IngestionKeys/deleteIngestionKey'; import createLimitForIngestionKeyApi from 'api/IngestionKeys/limits/createLimitsForKey'; import deleteLimitsForIngestionKey from 'api/IngestionKeys/limits/deleteLimitsForIngestionKey'; import updateLimitForIngestionKeyApi from 'api/IngestionKeys/limits/updateLimitsForIngestionKey'; import updateIngestionKey from 'api/IngestionKeys/updateIngestionKey'; import { AxiosError } from 'axios'; import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig'; import Tags from 'components/Tags/Tags'; import { SOMETHING_WENT_WRONG } from 'constants/api'; import dayjs from 'dayjs'; import { useGetAllIngestionsKeys } from 'hooks/IngestionKeys/useGetAllIngestionKeys'; import useDebouncedFn from 'hooks/useDebouncedFunction'; import { useNotifications } from 'hooks/useNotifications'; import { isNil, isUndefined } from 'lodash-es'; import { ArrowUpRight, CalendarClock, Check, Copy, Infinity, Info, Minus, PenLine, Plus, PlusIcon, Search, Trash2, X, } from 'lucide-react'; import { useTimezone } from 'providers/Timezone'; import { ChangeEvent, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useMutation } from 'react-query'; import { useSelector } from 'react-redux'; import { useCopyToClipboard } from 'react-use'; import { AppState } from 'store/reducers'; import { ErrorResponse } from 'types/api'; import { LimitProps } from 'types/api/ingestionKeys/limits/types'; import { IngestionKeyProps, PaginationProps, } from 'types/api/ingestionKeys/types'; import AppReducer from 'types/reducer/app'; import { USER_ROLES } from 'types/roles'; const { Option } = Select; const BYTES = 1073741824; // Using any type here because antd's DatePicker expects its own internal Dayjs type // which conflicts with our project's Dayjs type that has additional plugins (tz, utc etc). // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types export const disabledDate = (current: any): boolean => // Disable all dates before today current && current < dayjs().endOf('day'); const SIGNALS = ['logs', 'traces', 'metrics']; export const showErrorNotification = ( notifications: NotificationInstance, err: Error, ): void => { notifications.error({ message: err.message || SOMETHING_WENT_WRONG, }); }; type ExpiryOption = { value: string; label: string; }; export 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' }, ]; function MultiIngestionSettings(): JSX.Element { const { user } = useSelector((state) => state.app); const { notifications } = useNotifications(); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isDeleteLimitModalOpen, setIsDeleteLimitModalOpen] = useState(false); const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [, handleCopyToClipboard] = useCopyToClipboard(); const [updatedTags, setUpdatedTags] = useState([]); const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [isEditAddLimitOpen, setIsEditAddLimitOpen] = useState(false); const [activeAPIKey, setActiveAPIKey] = useState(); const [activeSignal, setActiveSignal] = useState(null); const [searchValue, setSearchValue] = useState(''); const [searchText, setSearchText] = useState(''); const [dataSource, setDataSource] = useState([]); const [paginationParams, setPaginationParams] = useState({ page: 1, per_page: 10, }); const [totalIngestionKeys, setTotalIngestionKeys] = useState(0); const [ hasCreateLimitForIngestionKeyError, setHasCreateLimitForIngestionKeyError, ] = useState(false); const [ createLimitForIngestionKeyError, setCreateLimitForIngestionKeyError, ] = useState(null); const [ hasUpdateLimitForIngestionKeyError, setHasUpdateLimitForIngestionKeyError, ] = useState(false); const [ updateLimitForIngestionKeyError, setUpdateLimitForIngestionKeyError, ] = useState(null); const { t } = useTranslation(['ingestionKeys']); const [editForm] = Form.useForm(); const [addEditLimitForm] = Form.useForm(); const [createForm] = Form.useForm(); const handleFormReset = (): void => { editForm.resetFields(); createForm.resetFields(); addEditLimitForm.resetFields(); }; const hideDeleteViewModal = (): void => { setIsDeleteModalOpen(false); setActiveAPIKey(null); handleFormReset(); }; const showDeleteModal = (apiKey: IngestionKeyProps): void => { setActiveAPIKey(apiKey); setIsDeleteModalOpen(true); }; const hideEditViewModal = (): void => { setActiveAPIKey(null); setIsEditModalOpen(false); handleFormReset(); }; const hideAddViewModal = (): void => { handleFormReset(); setActiveAPIKey(null); setIsAddModalOpen(false); }; const showEditModal = (apiKey: IngestionKeyProps): void => { setActiveAPIKey(apiKey); handleFormReset(); setUpdatedTags(apiKey.tags || []); editForm.setFieldsValue({ name: apiKey.name, tags: apiKey.tags, expires_at: dayjs(apiKey?.expires_at) || null, }); setIsEditModalOpen(true); }; const showAddModal = (): void => { setUpdatedTags([]); setActiveAPIKey(null); setIsAddModalOpen(true); }; const handleModalClose = (): void => { setActiveAPIKey(null); setActiveSignal(null); }; const { data: IngestionKeys, isLoading, isRefetching, refetch: refetchAPIKeys, error, isError, } = useGetAllIngestionsKeys({ search: searchText, ...paginationParams, }); useEffect(() => { setActiveAPIKey(IngestionKeys?.data.data[0]); }, [IngestionKeys]); useEffect(() => { setDataSource(IngestionKeys?.data.data || []); setTotalIngestionKeys(IngestionKeys?.data?._pagination?.total || 0); // eslint-disable-next-line react-hooks/exhaustive-deps }, [IngestionKeys?.data?.data]); useEffect(() => { if (isError) { showErrorNotification(notifications, error as AxiosError); } }, [error, isError, notifications]); const handleDebouncedSearch = useDebouncedFn((searchText): void => { setSearchText(searchText as string); }, 500); const handleSearch = (e: ChangeEvent): void => { setSearchValue(e.target.value); handleDebouncedSearch(e.target.value || ''); }; const clearSearch = (): void => { setSearchValue(''); }; const { mutate: createIngestionKey, isLoading: isLoadingCreateAPIKey, } = useMutation(createIngestionKeyApi, { onSuccess: (data) => { setActiveAPIKey(data.payload); setUpdatedTags([]); hideAddViewModal(); refetchAPIKeys(); }, onError: (error) => { showErrorNotification(notifications, error as AxiosError); }, }); const { mutate: updateAPIKey, isLoading: isLoadingUpdateAPIKey } = useMutation( updateIngestionKey, { onSuccess: () => { refetchAPIKeys(); setIsEditModalOpen(false); }, onError: (error) => { showErrorNotification(notifications, error as AxiosError); }, }, ); const { mutate: deleteAPIKey, isLoading: isDeleteingAPIKey } = useMutation( deleteIngestionKey, { onSuccess: () => { refetchAPIKeys(); setIsDeleteModalOpen(false); }, onError: (error) => { showErrorNotification(notifications, error as AxiosError); }, }, ); const { mutate: createLimitForIngestionKey, isLoading: isLoadingLimitForKey, } = useMutation(createLimitForIngestionKeyApi, { onSuccess: () => { setActiveSignal(null); setActiveAPIKey(null); setIsEditAddLimitOpen(false); setUpdatedTags([]); hideAddViewModal(); refetchAPIKeys(); setHasCreateLimitForIngestionKeyError(false); }, onError: (error: ErrorResponse) => { setHasCreateLimitForIngestionKeyError(true); setCreateLimitForIngestionKeyError(error); }, }); const { mutate: updateLimitForIngestionKey, isLoading: isLoadingUpdatedLimitForKey, } = useMutation(updateLimitForIngestionKeyApi, { onSuccess: () => { setActiveSignal(null); setActiveAPIKey(null); setIsEditAddLimitOpen(false); setUpdatedTags([]); hideAddViewModal(); refetchAPIKeys(); setHasUpdateLimitForIngestionKeyError(false); }, onError: (error: ErrorResponse) => { setHasUpdateLimitForIngestionKeyError(true); setUpdateLimitForIngestionKeyError(error); }, }); const { mutate: deleteLimitForKey, isLoading: isDeletingLimit } = useMutation( deleteLimitsForIngestionKey, { onSuccess: () => { setIsDeleteModalOpen(false); setIsDeleteLimitModalOpen(false); refetchAPIKeys(); }, 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, tags: updatedTags, expires_at: dayjs(values.expires_at).endOf('day').toISOString(), }, }); } }) .catch((errorInfo) => { console.error('error info', errorInfo); }); }; const onCreateIngestionKey = (): void => { createForm .validateFields() .then((values) => { if (user) { const requestPayload = { name: values.name, tags: updatedTags, expires_at: dayjs(values.expires_at).endOf('day').toISOString(), }; createIngestionKey(requestPayload); } }) .catch((errorInfo) => { console.error('error info', errorInfo); }); }; const handleCopyKey = (text: string): void => { handleCopyToClipboard(text); notifications.success({ message: 'Copied to clipboard', }); }; const gbToBytes = (gb: number): number => Math.round(gb * 1024 ** 3); const getFormattedTime = ( date: string, formatTimezoneAdjustedTimestamp: (date: string, format: string) => string, ): string => formatTimezoneAdjustedTimestamp(date, 'MMM DD,YYYY, hh:mm a (UTC Z)'); const showDeleteLimitModal = ( APIKey: IngestionKeyProps, limit: LimitProps, ): void => { setActiveAPIKey(APIKey); setActiveSignal(limit); setIsDeleteLimitModalOpen(true); }; const hideDeleteLimitModal = (): void => { setIsDeleteLimitModalOpen(false); }; const handleDiscardSaveLimit = (): void => { setHasCreateLimitForIngestionKeyError(false); setHasUpdateLimitForIngestionKeyError(false); setIsEditAddLimitOpen(false); setActiveAPIKey(null); setActiveSignal(null); addEditLimitForm.resetFields(); }; const handleAddLimit = ( APIKey: IngestionKeyProps, signalName: string, ): void => { const { dailyLimit, secondsLimit } = addEditLimitForm.getFieldsValue(); const payload = { keyID: APIKey.id, signal: signalName, config: {}, }; if (!isUndefined(dailyLimit)) { payload.config = { day: { size: gbToBytes(dailyLimit), }, }; } if (!isUndefined(secondsLimit)) { payload.config = { ...payload.config, second: { size: gbToBytes(secondsLimit), }, }; } if (isUndefined(dailyLimit) && isUndefined(secondsLimit)) { // No need to save as no limit is provided, close the edit view and reset active signal and api key setActiveSignal(null); setActiveAPIKey(null); setIsEditAddLimitOpen(false); setUpdatedTags([]); hideAddViewModal(); setHasCreateLimitForIngestionKeyError(false); return; } createLimitForIngestionKey(payload); }; const handleUpdateLimit = ( APIKey: IngestionKeyProps, signal: LimitProps, ): void => { const { dailyLimit, secondsLimit } = addEditLimitForm.getFieldsValue(); const payload = { limitID: signal.id, signal: signal.signal, config: {}, }; if (isUndefined(dailyLimit) && isUndefined(secondsLimit)) { showDeleteLimitModal(APIKey, signal); return; } if (!isUndefined(dailyLimit)) { payload.config = { day: { size: gbToBytes(dailyLimit), }, }; } if (!isUndefined(secondsLimit)) { payload.config = { ...payload.config, second: { size: gbToBytes(secondsLimit), }, }; } updateLimitForIngestionKey(payload); }; const bytesToGb = (size: number | undefined): number => { if (!size) { return 0; } return size / BYTES; }; const enableEditLimitMode = ( APIKey: IngestionKeyProps, signal: LimitProps, ): void => { setActiveAPIKey(APIKey); setActiveSignal({ ...signal, config: { ...signal.config, day: { ...signal.config?.day, enabled: !isNil(signal?.config?.day?.size), }, second: { ...signal.config?.second, enabled: !isNil(signal?.config?.second?.size), }, }, }); addEditLimitForm.setFieldsValue({ dailyLimit: bytesToGb(signal?.config?.day?.size || 0), secondsLimit: bytesToGb(signal?.config?.second?.size || 0), enableDailyLimit: !isNil(signal?.config?.day?.size), enableSecondLimit: !isNil(signal?.config?.second?.size), }); setIsEditAddLimitOpen(true); }; const onDeleteLimitHandler = (): void => { if (activeSignal && activeSignal?.id) { deleteLimitForKey(activeSignal.id); } }; const { formatTimezoneAdjustedTimestamp } = useTimezone(); const columns: AntDTableProps['columns'] = [ { title: 'Ingestion Key', key: 'ingestion-key', // eslint-disable-next-line sonarjs/cognitive-complexity render: (APIKey: IngestionKeyProps): JSX.Element => { const createdOn = getFormattedTime( APIKey.created_at, formatTimezoneAdjustedTimestamp, ); const formattedDateAndTime = APIKey && APIKey?.expires_at && getFormattedTime(APIKey?.expires_at, formatTimezoneAdjustedTimestamp); const updatedOn = getFormattedTime( APIKey?.updated_at, formatTimezoneAdjustedTimestamp, ); const limits: { [key: string]: LimitProps } = {}; APIKey.limits?.forEach((limit: LimitProps) => { limits[limit.signal] = limit; }); const hasLimits = (signal: string): boolean => !!limits[signal]; const items: CollapseProps['items'] = [ { key: '1', label: (
{APIKey?.name}
{APIKey?.value.substring(0, 2)}******** {APIKey?.value.substring(APIKey.value.length - 2).trim()} { e.stopPropagation(); e.preventDefault(); handleCopyKey(APIKey.value); }} />
), children: (
Created on {createdOn} {updatedOn && ( Updated on {updatedOn} )} {APIKey.tags && Array.isArray(APIKey.tags) && APIKey.tags.length > 0 && ( Tags
{APIKey.tags.map((tag, index) => ( // eslint-disable-next-line react/no-array-index-key {tag} ))}
)}

LIMITS

{SIGNALS.map((signal) => { const hasValidDayLimit = !isNil(limits[signal]?.config?.day?.size); const hasValidSecondLimit = !isNil( limits[signal]?.config?.second?.size, ); return (
{signal}
{hasLimits(signal) ? ( <> )}
{activeAPIKey?.id === APIKey.id && activeSignal?.signal === signal && isEditAddLimitOpen ? (
Daily limit
{ setActiveSignal({ ...activeSignal, config: { ...activeSignal.config, day: { ...activeSignal.config?.day, enabled: value, }, }, }); }} />
Add a limit for data ingested daily
{activeSignal?.config?.day?.enabled ? ( } /> ) : (
NO LIMIT
)}
Per Second limit{' '}
{ setActiveSignal({ ...activeSignal, config: { ...activeSignal.config, second: { ...activeSignal.config?.second, enabled: value, }, }, }); }} />
Add a limit for data ingested every second
{activeSignal?.config?.second?.enabled ? ( } /> ) : (
NO LIMIT
)}
{activeAPIKey?.id === APIKey.id && activeSignal.signal === signal && !isLoadingLimitForKey && hasCreateLimitForIngestionKeyError && createLimitForIngestionKeyError && createLimitForIngestionKeyError?.error && (
{createLimitForIngestionKeyError?.error}
)} {activeAPIKey?.id === APIKey.id && activeSignal.signal === signal && !isLoadingLimitForKey && hasUpdateLimitForIngestionKeyError && updateLimitForIngestionKeyError && (
{updateLimitForIngestionKeyError?.error}
)} {activeAPIKey?.id === APIKey.id && activeSignal.signal === signal && isEditAddLimitOpen && (
)}
) : (
Daily {' '}
{hasValidDayLimit ? ( <> {getYAxisFormattedValue( (limits[signal]?.metric?.day?.size || 0).toString(), 'bytes', )}{' '} /{' '} {getYAxisFormattedValue( (limits[signal]?.config?.day?.size || 0).toString(), 'bytes', )} ) : ( <> NO LIMIT )}
Seconds
{hasValidSecondLimit ? ( <> {getYAxisFormattedValue( (limits[signal]?.metric?.second?.size || 0).toString(), 'bytes', )}{' '} /{' '} {getYAxisFormattedValue( (limits[signal]?.config?.second?.size || 0).toString(), 'bytes', )} ) : ( <> NO LIMIT )}
)}
); })}
), }, ]; return (
Expires on {formattedDateAndTime}
); }, }, ]; const handleTableChange = (pagination: TablePaginationConfig): void => { setPaginationParams({ page: pagination?.current || 1, per_page: 10, }); }; return (
Find your ingestion URL and learn more about sending data to SigNoz{' '} here
Ingestion Keys Create and manage ingestion keys for the SigNoz Cloud{' '} {' '} Learn more
} value={searchValue} onChange={handleSearch} />
`${range[0]}-${range[1]} of ${total} Ingestion keys`, total: totalIngestionKeys, }} /> {/* Delete Key Modal */} Delete Ingestion Key} open={isDeleteModalOpen} closable afterClose={handleModalClose} onCancel={hideDeleteViewModal} destroyOnClose footer={[ , , ]} > {t('delete_confirm_message', { keyName: activeAPIKey?.name, })} {/* Delete Limit Modal */} Delete Limit } open={isDeleteLimitModalOpen} closable afterClose={handleModalClose} onCancel={hideDeleteLimitModal} destroyOnClose footer={[ , , ]} > {t('delete_limit_confirm_message', { limit_name: activeSignal?.signal, keyName: activeAPIKey?.name, })} {/* Edit Modal */} } > Cancel , , ]} >
{/* Create New Key Modal */} } > Cancel , , ]} >
); } export default MultiIngestionSettings;