diff --git a/frontend/src/container/IngestionSettings/MultiIngestionSettings.tsx b/frontend/src/container/IngestionSettings/MultiIngestionSettings.tsx index 6c817a41da..1d8383eaf8 100644 --- a/frontend/src/container/IngestionSettings/MultiIngestionSettings.tsx +++ b/frontend/src/container/IngestionSettings/MultiIngestionSettings.tsx @@ -58,7 +58,11 @@ import { useTranslation } from 'react-i18next'; import { useMutation } from 'react-query'; import { useCopyToClipboard } from 'react-use'; import { ErrorResponse } from 'types/api'; -import { LimitProps } from 'types/api/ingestionKeys/limits/types'; +import { + AddLimitProps, + LimitProps, + UpdateLimitProps, +} from 'types/api/ingestionKeys/limits/types'; import { IngestionKeyProps, PaginationProps, @@ -69,6 +73,18 @@ const { Option } = Select; const BYTES = 1073741824; +const COUNT_MULTIPLIER = { + thousand: 1000, + million: 1000000, + billion: 1000000000, +}; + +const SIGNALS_CONFIG = [ + { name: 'logs', usesSize: true, usesCount: false }, + { name: 'traces', usesSize: true, usesCount: false }, + { name: 'metrics', usesSize: false, usesCount: true }, +]; + // 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 @@ -76,8 +92,6 @@ 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, @@ -101,6 +115,31 @@ export const API_KEY_EXPIRY_OPTIONS: ExpiryOption[] = [ { value: '0', label: 'No Expiry' }, ]; +const countToUnit = (count: number): { value: number; unit: string } => { + if ( + count >= COUNT_MULTIPLIER.billion || + count / COUNT_MULTIPLIER.million >= 1000 + ) { + return { value: count / COUNT_MULTIPLIER.billion, unit: 'billion' }; + } + if ( + count >= COUNT_MULTIPLIER.million || + count / COUNT_MULTIPLIER.thousand >= 1000 + ) { + return { value: count / COUNT_MULTIPLIER.million, unit: 'million' }; + } + if (count >= COUNT_MULTIPLIER.thousand) { + return { value: count / COUNT_MULTIPLIER.thousand, unit: 'thousand' }; + } + // Default to million for small numbers + return { value: count / COUNT_MULTIPLIER.million, unit: 'million' }; +}; + +const countFromUnit = (value: number, unit: string): number => + value * + (COUNT_MULTIPLIER[unit as keyof typeof COUNT_MULTIPLIER] || + COUNT_MULTIPLIER.million); + function MultiIngestionSettings(): JSX.Element { const { user } = useAppContext(); const { notifications } = useNotifications(); @@ -181,7 +220,6 @@ function MultiIngestionSettings(): JSX.Element { const showEditModal = (apiKey: IngestionKeyProps): void => { setActiveAPIKey(apiKey); - handleFormReset(); setUpdatedTags(apiKey.tags || []); @@ -424,44 +462,90 @@ function MultiIngestionSettings(): JSX.Element { addEditLimitForm.resetFields(); }; + /* eslint-disable sonarjs/cognitive-complexity */ const handleAddLimit = ( APIKey: IngestionKeyProps, signalName: string, ): void => { - const { dailyLimit, secondsLimit } = addEditLimitForm.getFieldsValue(); - - const payload = { + const { + dailyLimit, + secondsLimit, + dailyCount, + dailyCountUnit, + secondsCount, + secondsCountUnit, + } = addEditLimitForm.getFieldsValue(); + + const payload: AddLimitProps = { keyID: APIKey.id, signal: signalName, config: {}, }; - if (!isUndefined(dailyLimit)) { - payload.config = { - day: { + const signalCfg = SIGNALS_CONFIG.find((cfg) => cfg.name === signalName); + if (!signalCfg) return; + + // Only set size if usesSize is true + if (signalCfg.usesSize) { + if (!isUndefined(dailyLimit)) { + payload.config.day = { + ...payload.config.day, size: gbToBytes(dailyLimit), - }, - }; + }; + } + if (!isUndefined(secondsLimit)) { + payload.config.second = { + ...payload.config.second, + size: gbToBytes(secondsLimit), + }; + } } - if (!isUndefined(secondsLimit)) { - payload.config = { - ...payload.config, - second: { - size: gbToBytes(secondsLimit), - }, - }; + // Only set count if usesCount is true + if (signalCfg.usesCount) { + if (!isUndefined(dailyCount)) { + payload.config.day = { + ...payload.config.day, + count: countFromUnit(dailyCount, dailyCountUnit || 'million'), + }; + } + if (!isUndefined(secondsCount)) { + payload.config.second = { + ...payload.config.second, + count: countFromUnit(secondsCount, secondsCountUnit || 'million'), + }; + } } - 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 + // If neither size nor count was given, skip + const noSizeProvided = + isUndefined(dailyLimit) && isUndefined(secondsLimit) && signalCfg.usesSize; + const noCountProvided = + isUndefined(dailyCount) && isUndefined(secondsCount) && signalCfg.usesCount; + + if ( + signalCfg.usesSize && + signalCfg.usesCount && + noSizeProvided && + noCountProvided + ) { + // Both size and count are effectively empty setActiveSignal(null); setActiveAPIKey(null); setIsEditAddLimitOpen(false); setUpdatedTags([]); hideAddViewModal(); setHasCreateLimitForIngestionKeyError(false); + return; + } + if (!signalCfg.usesSize && !signalCfg.usesCount) { + // Edge case: If there's no count or size usage at all + setActiveSignal(null); + setActiveAPIKey(null); + setIsEditAddLimitOpen(false); + setUpdatedTags([]); + hideAddViewModal(); return; } @@ -472,44 +556,73 @@ function MultiIngestionSettings(): JSX.Element { APIKey: IngestionKeyProps, signal: LimitProps, ): void => { - const { dailyLimit, secondsLimit } = addEditLimitForm.getFieldsValue(); - const payload = { + const { + dailyLimit, + secondsLimit, + dailyCount, + dailyCountUnit, + secondsCount, + secondsCountUnit, + } = addEditLimitForm.getFieldsValue(); + + const payload: UpdateLimitProps = { limitID: signal.id, signal: signal.signal, config: {}, }; - if (isUndefined(dailyLimit) && isUndefined(secondsLimit)) { - showDeleteLimitModal(APIKey, signal); + const signalCfg = SIGNALS_CONFIG.find((cfg) => cfg.name === signal.signal); + if (!signalCfg) return; + const noSizeProvided = + isUndefined(dailyLimit) && isUndefined(secondsLimit) && signalCfg.usesSize; + const noCountProvided = + isUndefined(dailyCount) && isUndefined(secondsCount) && signalCfg.usesCount; + + // If the user cleared out all fields, remove the limit + if (noSizeProvided && noCountProvided) { + showDeleteLimitModal(APIKey, signal); return; } - if (!isUndefined(dailyLimit)) { - payload.config = { - day: { + if (signalCfg.usesSize) { + if (!isUndefined(dailyLimit)) { + payload.config.day = { + ...payload.config.day, size: gbToBytes(dailyLimit), - }, - }; + }; + } + if (!isUndefined(secondsLimit)) { + payload.config.second = { + ...payload.config.second, + size: gbToBytes(secondsLimit), + }; + } } - if (!isUndefined(secondsLimit)) { - payload.config = { - ...payload.config, - second: { - size: gbToBytes(secondsLimit), - }, - }; + if (signalCfg.usesCount) { + if (!isUndefined(dailyCount)) { + payload.config.day = { + ...payload.config.day, + count: countFromUnit(dailyCount, dailyCountUnit || 'million'), + }; + } + if (!isUndefined(secondsCount)) { + payload.config.second = { + ...payload.config.second, + count: countFromUnit(secondsCount, secondsCountUnit || 'million'), + }; + } } updateLimitForIngestionKey(payload); }; + /* eslint-enable sonarjs/cognitive-complexity */ const bytesToGb = (size: number | undefined): number => { if (!size) { return 0; } - return size / BYTES; }; @@ -517,6 +630,12 @@ function MultiIngestionSettings(): JSX.Element { APIKey: IngestionKeyProps, signal: LimitProps, ): void => { + const dayCount = signal?.config?.day?.count; + const secondCount = signal?.config?.second?.count; + + const dayCountConverted = countToUnit(dayCount || 0); + const secondCountConverted = countToUnit(secondCount || 0); + setActiveAPIKey(APIKey); setActiveSignal({ ...signal, @@ -524,11 +643,14 @@ function MultiIngestionSettings(): JSX.Element { ...signal.config, day: { ...signal.config?.day, - enabled: !isNil(signal?.config?.day?.size), + enabled: + !isNil(signal?.config?.day?.size) || !isNil(signal?.config?.day?.count), }, second: { ...signal.config?.second, - enabled: !isNil(signal?.config?.second?.size), + enabled: + !isNil(signal?.config?.second?.size) || + !isNil(signal?.config?.second?.count), }, }, }); @@ -536,15 +658,22 @@ function MultiIngestionSettings(): JSX.Element { 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), + enableDailyLimit: + !isNil(signal?.config?.day?.size) || !isNil(signal?.config?.day?.count), + enableSecondLimit: + !isNil(signal?.config?.second?.size) || + !isNil(signal?.config?.second?.count), + dailyCount: dayCountConverted.value, + dailyCountUnit: dayCountConverted.unit, + secondsCount: secondCountConverted.value, + secondsCountUnit: secondCountConverted.unit, }); setIsEditAddLimitOpen(true); }; const onDeleteLimitHandler = (): void => { - if (activeSignal && activeSignal?.id) { + if (activeSignal && activeSignal.id) { deleteLimitForKey(activeSignal.id); } }; @@ -572,13 +701,13 @@ function MultiIngestionSettings(): JSX.Element { formatTimezoneAdjustedTimestamp, ); - const limits: { [key: string]: LimitProps } = {}; - - APIKey.limits?.forEach((limit: LimitProps) => { - limits[limit.signal] = limit; + // Convert array of limits to a dictionary for quick access + const limitsDict: Record = {}; + APIKey.limits?.forEach((limitItem: LimitProps) => { + limitsDict[limitItem.signal] = limitItem; }); - const hasLimits = (signal: string): boolean => !!limits[signal]; + const hasLimits = (signalName: string): boolean => !!limitsDict[signalName]; const items: CollapseProps['items'] = [ { @@ -614,11 +743,9 @@ function MultiIngestionSettings(): JSX.Element { onClick={(e): void => { e.stopPropagation(); e.preventDefault(); - showEditModal(APIKey); }} /> -