From 6deac52b8d44067c0ebd19d87d804f419634cc4c Mon Sep 17 00:00:00 2001 From: Yunus A M Date: Wed, 29 May 2024 17:47:15 +0530 Subject: [PATCH 01/14] feat: support multi ingestion keys --- frontend/package.json | 1 + .../public/locales/en-GB/ingestionKeys.json | 3 + frontend/public/locales/en/ingestionKeys.json | 3 + frontend/src/api/ErrorResponseHandler.ts | 2 +- .../api/IngestionKeys/createIngestionKey.ts | 31 + .../api/IngestionKeys/deleteIngestionKey.ts | 26 + .../api/IngestionKeys/getAllIngestionKeys.ts | 7 + .../api/IngestionKeys/updateIngestionKey.ts | 32 + frontend/src/api/apiV1.ts | 1 + frontend/src/api/index.ts | 21 +- frontend/src/components/Tags/Tags.styles.scss | 38 + frontend/src/components/Tags/Tags.tsx | 139 ++++ .../IngestionSettings.styles.scss | 692 +++++++++++++++++ .../IngestionSettings/IngestionSettings.tsx | 721 ++++++++++++++++-- .../General/AddTags/index.tsx | 2 +- .../IngestionKeys/useGetAllIngestionKeys.ts | 13 + frontend/src/types/api/ingestionKeys/types.ts | 57 ++ frontend/yarn.lock | 99 ++- 18 files changed, 1813 insertions(+), 75 deletions(-) create mode 100644 frontend/public/locales/en-GB/ingestionKeys.json create mode 100644 frontend/public/locales/en/ingestionKeys.json create mode 100644 frontend/src/api/IngestionKeys/createIngestionKey.ts create mode 100644 frontend/src/api/IngestionKeys/deleteIngestionKey.ts create mode 100644 frontend/src/api/IngestionKeys/getAllIngestionKeys.ts create mode 100644 frontend/src/api/IngestionKeys/updateIngestionKey.ts create mode 100644 frontend/src/components/Tags/Tags.styles.scss create mode 100644 frontend/src/components/Tags/Tags.tsx create mode 100644 frontend/src/hooks/IngestionKeys/useGetAllIngestionKeys.ts create mode 100644 frontend/src/types/api/ingestionKeys/types.ts diff --git a/frontend/package.json b/frontend/package.json index 918c2ed416f..ee32d3dee7a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -88,6 +88,7 @@ "lucide-react": "0.379.0", "mini-css-extract-plugin": "2.4.5", "papaparse": "5.4.1", + "rc-tween-one": "3.0.6", "react": "18.2.0", "react-addons-update": "15.6.3", "react-beautiful-dnd": "13.1.1", diff --git a/frontend/public/locales/en-GB/ingestionKeys.json b/frontend/public/locales/en-GB/ingestionKeys.json new file mode 100644 index 00000000000..256e88391a4 --- /dev/null +++ b/frontend/public/locales/en-GB/ingestionKeys.json @@ -0,0 +1,3 @@ +{ + "delete_confirm_message": "Are you sure you want to delete {{keyName}}? Deleting an ingestion key is irreversible and cannot be undone." +} diff --git a/frontend/public/locales/en/ingestionKeys.json b/frontend/public/locales/en/ingestionKeys.json new file mode 100644 index 00000000000..256e88391a4 --- /dev/null +++ b/frontend/public/locales/en/ingestionKeys.json @@ -0,0 +1,3 @@ +{ + "delete_confirm_message": "Are you sure you want to delete {{keyName}}? Deleting an ingestion key is irreversible and cannot be undone." +} diff --git a/frontend/src/api/ErrorResponseHandler.ts b/frontend/src/api/ErrorResponseHandler.ts index 027418ec845..be2dd5e31af 100644 --- a/frontend/src/api/ErrorResponseHandler.ts +++ b/frontend/src/api/ErrorResponseHandler.ts @@ -16,7 +16,7 @@ export function ErrorResponseHandler(error: AxiosError): ErrorResponse { return { statusCode, payload: null, - error: data.errorType, + error: data.errorType || data.type, message: null, }; } diff --git a/frontend/src/api/IngestionKeys/createIngestionKey.ts b/frontend/src/api/IngestionKeys/createIngestionKey.ts new file mode 100644 index 00000000000..a2c474cdb1d --- /dev/null +++ b/frontend/src/api/IngestionKeys/createIngestionKey.ts @@ -0,0 +1,31 @@ +import { GatewayApiV1Instance } from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { + CreateIngestionKeyProps, + IngestionKeyProps, +} from 'types/api/ingestionKeys/types'; + +const createIngestionKey = async ( + props: CreateIngestionKeyProps, +): Promise | ErrorResponse> => { + try { + console.log('props', props); + + const response = await GatewayApiV1Instance.post('/workspaces/me/keys', { + ...props, + }); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default createIngestionKey; diff --git a/frontend/src/api/IngestionKeys/deleteIngestionKey.ts b/frontend/src/api/IngestionKeys/deleteIngestionKey.ts new file mode 100644 index 00000000000..5f4e7e02c7d --- /dev/null +++ b/frontend/src/api/IngestionKeys/deleteIngestionKey.ts @@ -0,0 +1,26 @@ +import { GatewayApiV1Instance } from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { AllIngestionKeyProps } from 'types/api/ingestionKeys/types'; + +const deleteIngestionKey = async ( + id: string, +): Promise | ErrorResponse> => { + try { + const response = await GatewayApiV1Instance.delete( + `/workspaces/me/keys/${id}`, + ); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default deleteIngestionKey; diff --git a/frontend/src/api/IngestionKeys/getAllIngestionKeys.ts b/frontend/src/api/IngestionKeys/getAllIngestionKeys.ts new file mode 100644 index 00000000000..b1ee6dc933b --- /dev/null +++ b/frontend/src/api/IngestionKeys/getAllIngestionKeys.ts @@ -0,0 +1,7 @@ +import { GatewayApiV1Instance } from 'api'; +import { AxiosResponse } from 'axios'; +import { AllIngestionKeyProps } from 'types/api/ingestionKeys/types'; + +export const getAllIngestionKeys = (): Promise< + AxiosResponse +> => GatewayApiV1Instance.get(`/workspaces/me/keys`); diff --git a/frontend/src/api/IngestionKeys/updateIngestionKey.ts b/frontend/src/api/IngestionKeys/updateIngestionKey.ts new file mode 100644 index 00000000000..c4777ef97f4 --- /dev/null +++ b/frontend/src/api/IngestionKeys/updateIngestionKey.ts @@ -0,0 +1,32 @@ +import { GatewayApiV1Instance } from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { + IngestionKeysPayloadProps, + UpdateIngestionKeyProps, +} from 'types/api/ingestionKeys/types'; + +const updateIngestionKey = async ( + props: UpdateIngestionKeyProps, +): Promise | ErrorResponse> => { + try { + const response = await GatewayApiV1Instance.patch( + `/workspaces/me/keys/${props.id}`, + { + ...props.data, + }, + ); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default updateIngestionKey; diff --git a/frontend/src/api/apiV1.ts b/frontend/src/api/apiV1.ts index 4fba137e182..05b4e62e78a 100644 --- a/frontend/src/api/apiV1.ts +++ b/frontend/src/api/apiV1.ts @@ -3,6 +3,7 @@ const apiV1 = '/api/v1/'; export const apiV2 = '/api/v2/'; export const apiV3 = '/api/v3/'; export const apiV4 = '/api/v4/'; +export const gatewayApiV1 = '/api/gateway/v1'; export const apiAlertManager = '/api/alertmanager'; export default apiV1; diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 92a06363a10..1ec4cda6014 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -9,7 +9,13 @@ import { ENVIRONMENT } from 'constants/env'; import { LOCALSTORAGE } from 'constants/localStorage'; import store from 'store'; -import apiV1, { apiAlertManager, apiV2, apiV3, apiV4 } from './apiV1'; +import apiV1, { + apiAlertManager, + apiV2, + apiV3, + apiV4, + gatewayApiV1, +} from './apiV1'; import { Logout } from './utils'; const interceptorsResponse = ( @@ -134,6 +140,19 @@ ApiV4Instance.interceptors.response.use( ApiV4Instance.interceptors.request.use(interceptorsRequestResponse); // +// gateway Api V1 +export const GatewayApiV1Instance = axios.create({ + baseURL: `${ENVIRONMENT.baseURL}${gatewayApiV1}`, +}); + +GatewayApiV1Instance.interceptors.response.use( + interceptorsResponse, + interceptorRejected, +); + +GatewayApiV1Instance.interceptors.request.use(interceptorsRequestResponse); +// + AxiosAlertManagerInstance.interceptors.response.use( interceptorsResponse, interceptorRejected, diff --git a/frontend/src/components/Tags/Tags.styles.scss b/frontend/src/components/Tags/Tags.styles.scss new file mode 100644 index 00000000000..f2abbf07a20 --- /dev/null +++ b/frontend/src/components/Tags/Tags.styles.scss @@ -0,0 +1,38 @@ +.tags-container { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; + + .tags { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + } + + .ant-form-item { + margin-bottom: 0; + } + + .ant-tag { + margin-right: 0; + background: white; + } +} + +.add-tag-container { + display: flex; + align-items: center; + gap: 4px; + + .ant-form-item { + margin-bottom: 0; + } + + .confirm-cancel-actions { + display: flex; + align-items: center; + gap: 2px; + } +} diff --git a/frontend/src/components/Tags/Tags.tsx b/frontend/src/components/Tags/Tags.tsx new file mode 100644 index 00000000000..7a1a0359370 --- /dev/null +++ b/frontend/src/components/Tags/Tags.tsx @@ -0,0 +1,139 @@ +import './Tags.styles.scss'; + +import { PlusOutlined } from '@ant-design/icons'; +import { Button } from 'antd'; +import { Tag } from 'antd/lib'; +import Input from 'components/Input'; +import { Check, X } from 'lucide-react'; +import { TweenOneGroup } from 'rc-tween-one'; +import React, { Dispatch, SetStateAction, useState } from 'react'; + +function Tags({ tags, setTags }: AddTagsProps): JSX.Element { + const [inputValue, setInputValue] = useState(''); + const [inputVisible, setInputVisible] = useState(false); + + const handleInputConfirm = (): void => { + if (tags.indexOf(inputValue) > -1) { + return; + } + + if (inputValue) { + setTags([...tags, inputValue]); + } + setInputVisible(false); + setInputValue(''); + }; + + const handleClose = (removedTag: string): void => { + const newTags = tags.filter((tag) => tag !== removedTag); + setTags(newTags); + }; + + const showInput = (): void => { + setInputVisible(true); + setInputValue(''); + }; + + const hideInput = (): void => { + setInputValue(''); + setInputVisible(false); + }; + + const onChangeHandler = ( + value: string, + func: Dispatch>, + ): void => { + func(value); + }; + + const forMap = (tag: string): React.ReactElement => ( + + { + e.preventDefault(); + handleClose(tag); + }} + > + {tag} + + + ); + + const tagChild = tags.map(forMap); + + const renderTagsAnimated = (): React.ReactElement => ( + { + if (e.type === 'appear' || e.type === 'enter') { + (e.target as any).style = 'display: inline-block'; + } + }} + > + {tagChild} + + ); + + return ( +
+ {renderTagsAnimated()} + {inputVisible && ( +
+ + onChangeHandler(event.target.value, setInputValue) + } + // onBlurHandler={handleInputConfirm} + onPressEnterHandler={handleInputConfirm} + /> + +
+
+
+ )} + + {!inputVisible && ( + + )} +
+ ); +} + +interface AddTagsProps { + tags: string[]; + setTags: Dispatch>; +} + +export default Tags; diff --git a/frontend/src/container/IngestionSettings/IngestionSettings.styles.scss b/frontend/src/container/IngestionSettings/IngestionSettings.styles.scss index 3d5f41ab33b..77ec18635eb 100644 --- a/frontend/src/container/IngestionSettings/IngestionSettings.styles.scss +++ b/frontend/src/container/IngestionSettings/IngestionSettings.styles.scss @@ -1,3 +1,695 @@ .ingestion-settings-container { color: white; } + +.ingestion-key-container { + margin-top: 24px; + display: flex; + justify-content: center; + width: 100%; + + .ingestion-key-content { + width: calc(100% - 30px); + max-width: 736px; + + .title { + color: var(--bg-vanilla-100); + font-size: var(--font-size-lg); + font-style: normal; + font-weight: var(--font-weight-normal); + line-height: 28px; /* 155.556% */ + letter-spacing: -0.09px; + } + + .subtitle { + color: var(--bg-vanilla-400); + font-size: var(--font-size-sm); + font-style: normal; + font-weight: var(--font-weight-normal); + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + + .ingestion-keys-search-add-new { + display: flex; + align-items: center; + gap: 12px; + + padding: 16px 0; + + .add-new-ingestion-key-btn { + display: flex; + align-items: center; + gap: 8px; + } + } + + .ant-table-row { + .ant-table-cell { + padding: 0; + border: none; + background: var(--bg-ink-500); + } + .column-render { + margin: 8px 0 !important; + border-radius: 6px; + border: 1px solid var(--bg-slate-500); + background: var(--bg-ink-400); + + .title-with-action { + display: flex; + justify-content: space-between; + + align-items: center; + padding: 8px; + + .ingestion-key-data { + display: flex; + gap: 8px; + align-items: center; + + .ingestion-key-title { + display: flex; + align-items: center; + gap: 6px; + + .ant-typography { + color: var(--bg-vanilla-400); + font-size: var(--font-size-sm); + font-style: normal; + font-weight: var(--font-weight-medium); + line-height: 20px; + letter-spacing: -0.07px; + } + } + + .ingestion-key-value { + display: flex; + align-items: center; + gap: 12px; + + border-radius: 20px; + padding: 0px 12px; + + background: var(--bg-ink-200); + + .ant-typography { + color: var(--bg-vanilla-400); + font-size: var(--font-size-xs); + font-family: 'Space Mono', monospace; + font-style: normal; + font-weight: var(--font-weight-medium); + line-height: 20px; + letter-spacing: -0.07px; + } + + .copy-key-btn { + cursor: pointer; + } + } + } + + .action-btn { + display: flex; + align-items: center; + gap: 4px; + cursor: pointer; + } + + .visibility-btn { + border: 1px solid rgba(113, 144, 249, 0.2); + background: rgba(113, 144, 249, 0.1); + } + } + + .ant-collapse { + border: none; + + .ant-collapse-header { + padding: 0px 8px; + + display: flex; + align-items: center; + background-color: #121317; + } + + .ant-collapse-content { + border-top: 1px solid var(--bg-slate-500); + } + + .ant-collapse-item { + border-bottom: none; + } + + .ant-collapse-expand-icon { + padding-inline-end: 0px; + } + } + + .ingestion-key-details { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + border-top: 1px solid var(--bg-slate-500); + padding: 8px; + + .ingestion-key-tag { + width: 14px; + height: 14px; + border-radius: 50px; + background: var(--bg-slate-300); + display: flex; + justify-content: center; + align-items: center; + + .tag-text { + color: var(--bg-vanilla-400); + leading-trim: both; + text-edge: cap; + font-size: 10px; + font-style: normal; + font-weight: var(--font-weight-normal); + line-height: normal; + letter-spacing: -0.05px; + } + } + + .ingestion-key-created-by { + margin-left: 8px; + } + + .ingestion-key-tags-container { + display: flex; + align-items: center; + gap: 16px; + } + + .ingestion-key-last-used-at { + display: flex; + align-items: center; + gap: 8px; + + .ant-typography { + color: var(--bg-vanilla-400); + font-size: var(--font-size-sm); + font-style: normal; + font-weight: var(--font-weight-normal); + line-height: 18px; /* 128.571% */ + letter-spacing: -0.07px; + font-variant-numeric: lining-nums tabular-nums stacked-fractions + slashed-zero; + font-feature-settings: 'dlig' on, 'salt' on, 'cpsp' on, 'case' on; + } + } + + .ingestion-key-expires-in { + font-style: normal; + font-weight: 400; + line-height: 18px; + + display: flex; + align-items: center; + gap: 8px; + + .dot { + height: 6px; + width: 6px; + border-radius: 50%; + } + + &.warning { + color: var(--bg-amber-400); + + .dot { + background: var(--bg-amber-400); + box-shadow: 0px 0px 6px 0px var(--bg-amber-400); + } + } + + &.danger { + color: var(--bg-cherry-400); + + .dot { + background: var(--bg-cherry-400); + box-shadow: 0px 0px 6px 0px var(--bg-cherry-400); + } + } + } + } + } + } + + .ant-pagination-item { + display: flex; + justify-content: center; + align-items: center; + + > a { + color: var(--bg-vanilla-400); + font-variant-numeric: lining-nums tabular-nums slashed-zero; + font-feature-settings: 'dlig' on, 'salt' on, 'case' on, 'cpsp' on; + font-size: var(--font-size-sm); + font-style: normal; + font-weight: var(--font-weight-normal); + line-height: 20px; /* 142.857% */ + } + } + + .ant-pagination-item-active { + background-color: var(--bg-robin-500); + > a { + color: var(--bg-ink-500) !important; + font-size: var(--font-size-sm); + font-style: normal; + font-weight: var(--font-weight-medium); + line-height: 20px; + } + } + } +} + +.ingestion-key-info-container { + display: flex; + gap: 12px; + flex-direction: column; + + .user-info { + display: flex; + gap: 8px; + align-items: center; + flex-wrap: wrap; + + .user-avatar { + background-color: lightslategray; + vertical-align: middle; + } + } + + .user-email { + display: inline-flex; + align-items: center; + gap: 12px; + border-radius: 20px; + padding: 0px 12px; + background: var(--bg-ink-200); + + font-family: 'Space Mono', monospace; + } + + .role { + display: flex; + align-items: center; + gap: 12px; + } +} + +.ingestion-key-modal { + .ant-modal-content { + border-radius: 4px; + border: 1px solid var(--bg-slate-500); + background: var(--bg-ink-400); + box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2); + padding: 0; + + .ant-modal-header { + background: none; + border-bottom: 1px solid var(--bg-slate-500); + padding: 16px; + } + + .ant-modal-close-x { + font-size: 12px; + } + + .ant-modal-body { + padding: 12px 16px; + } + + .ant-modal-footer { + padding: 16px; + margin-top: 0; + + display: flex; + justify-content: flex-end; + } + } +} + +.ingestion-key-access-role { + display: flex; + + .ant-radio-button-wrapper { + font-size: 12px; + text-transform: capitalize; + + &.ant-radio-button-wrapper-checked { + color: #fff; + background: var(--bg-slate-400, #1d212d); + border-color: var(--bg-slate-400, #1d212d); + + &:hover { + color: #fff; + background: var(--bg-slate-400, #1d212d); + border-color: var(--bg-slate-400, #1d212d); + + &::before { + background-color: var(--bg-slate-400, #1d212d); + } + } + + &:focus { + color: #fff; + background: var(--bg-slate-400, #1d212d); + border-color: var(--bg-slate-400, #1d212d); + } + } + } + + .tab { + border: 1px solid var(--bg-slate-400); + + flex: 1; + + display: flex; + justify-content: center; + + &::before { + background: var(--bg-slate-400); + } + + &.selected { + background: var(--bg-slate-400, #1d212d); + } + } + + .role { + display: flex; + align-items: center; + gap: 8px; + } +} + +.delete-ingestion-key-modal { + width: calc(100% - 30px) !important; /* Adjust the 20px as needed */ + max-width: 384px; + .ant-modal-content { + padding: 0; + border-radius: 4px; + border: 1px solid var(--bg-slate-500); + background: var(--bg-ink-400); + box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2); + + .ant-modal-header { + padding: 16px; + background: var(--bg-ink-400); + } + + .ant-modal-body { + padding: 0px 16px 28px 16px; + + .ant-typography { + color: var(--bg-vanilla-400); + font-size: var(--font-size-sm); + font-style: normal; + font-weight: var(--font-weight-normal); + line-height: 20px; + letter-spacing: -0.07px; + } + + .ingestion-key-input { + margin-top: 8px; + display: flex; + gap: 8px; + } + + .ant-color-picker-trigger { + padding: 6px; + border-radius: 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + width: 32px; + height: 32px; + + .ant-color-picker-color-block { + border-radius: 50px; + width: 16px; + height: 16px; + flex-shrink: 0; + + .ant-color-picker-color-block-inner { + display: flex; + justify-content: center; + align-items: center; + } + } + } + } + + .ant-modal-footer { + display: flex; + justify-content: flex-end; + padding: 16px 16px; + margin: 0; + + .cancel-btn { + display: flex; + align-items: center; + border: none; + border-radius: 2px; + background: var(--bg-slate-500); + } + + .delete-btn { + display: flex; + align-items: center; + border: none; + border-radius: 2px; + background: var(--bg-cherry-500); + margin-left: 12px; + } + + .delete-btn:hover { + color: var(--bg-vanilla-100); + background: var(--bg-cherry-600); + } + } + } + + .title { + color: var(--bg-vanilla-100); + font-size: var(--font-size-sm); + font-style: normal; + font-weight: var(--font-weight-medium); + line-height: 20px; /* 142.857% */ + } +} + +.expiration-selector { + .ant-select-selector { + border: 1px solid var(--bg-slate-400) !important; + } +} + +.newAPIKeyDetails { + display: flex; + flex-direction: column; + gap: 8px; +} + +.copyable-text { + display: inline-flex; + align-items: center; + gap: 12px; + border-radius: 20px; + padding: 0px 12px; + background: var(--bg-ink-200, #23262e); + + .copy-key-btn { + cursor: pointer; + } +} + +.lightMode { + .ingestion-key-container { + .ingestion-key-content { + .title { + color: var(--bg-ink-500); + } + + .ant-table-row { + .ant-table-cell { + background: var(--bg-vanilla-200); + } + + &:hover { + .ant-table-cell { + background: var(--bg-vanilla-200) !important; + } + } + + .column-render { + border: 1px solid var(--bg-vanilla-200); + background: var(--bg-vanilla-100); + + .ant-collapse { + border: none; + + .ant-collapse-header { + background: var(--bg-vanilla-100); + } + + .ant-collapse-content { + border-top: 1px solid var(--bg-vanilla-300); + } + } + + .title-with-action { + .ingestion-key-title { + .ant-typography { + color: var(--bg-ink-500); + } + } + + .ingestion-key-value { + background: var(--bg-vanilla-200); + + .ant-typography { + color: var(--bg-slate-400); + } + + .copy-key-btn { + cursor: pointer; + } + } + + .action-btn { + .ant-typography { + color: var(--bg-ink-500); + } + } + } + + .ingestion-key-details { + border-top: 1px solid var(--bg-vanilla-200); + .ingestion-key-tag { + background: var(--bg-vanilla-200); + .tag-text { + color: var(--bg-ink-500); + } + } + + .ingestion-key-created-by { + color: var(--bg-ink-500); + } + + .ingestion-key-last-used-at { + .ant-typography { + color: var(--bg-ink-500); + } + } + } + } + } + } + } + + .delete-ingestion-key-modal { + .ant-modal-content { + border: 1px solid var(--bg-vanilla-200); + background: var(--bg-vanilla-100); + + .ant-modal-header { + background: var(--bg-vanilla-100); + + .title { + color: var(--bg-ink-500); + } + } + + .ant-modal-body { + .ant-typography { + color: var(--bg-ink-500); + } + + .ingestion-key-input { + .ant-input { + background: var(--bg-vanilla-200); + color: var(--bg-ink-500); + } + } + } + + .ant-modal-footer { + .cancel-btn { + background: var(--bg-vanilla-300); + color: var(--bg-ink-400); + } + } + } + } + + .ingestion-key-info-container { + .user-email { + background: var(--bg-vanilla-200); + } + } + + .ingestion-key-modal { + .ant-modal-content { + border-radius: 4px; + border: 1px solid var(--bg-vanilla-200); + background: var(--bg-vanilla-100); + box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2); + padding: 0; + + .ant-modal-header { + background: none; + border-bottom: 1px solid var(--bg-vanilla-200); + padding: 16px; + } + } + } + + .ingestion-key-access-role { + .ant-radio-button-wrapper { + &.ant-radio-button-wrapper-checked { + color: var(--bg-ink-400); + background: var(--bg-vanilla-300); + border-color: var(--bg-vanilla-300); + + &:hover { + color: var(--bg-ink-400); + background: var(--bg-vanilla-300); + border-color: var(--bg-vanilla-300); + + &::before { + background-color: var(--bg-vanilla-300); + } + } + + &:focus { + color: var(--bg-ink-400); + background: var(--bg-vanilla-300); + border-color: var(--bg-vanilla-300); + } + } + } + + .tab { + border: 1px solid var(--bg-vanilla-300); + + &::before { + background: var(--bg-vanilla-300); + } + + &.selected { + background: var(--bg-vanilla-300); + } + } + } + + .copyable-text { + background: var(--bg-vanilla-200); + } +} diff --git a/frontend/src/container/IngestionSettings/IngestionSettings.tsx b/frontend/src/container/IngestionSettings/IngestionSettings.tsx index c84543ca4e5..963b9b701f9 100644 --- a/frontend/src/container/IngestionSettings/IngestionSettings.tsx +++ b/frontend/src/container/IngestionSettings/IngestionSettings.tsx @@ -1,91 +1,680 @@ import './IngestionSettings.styles.scss'; -import { Skeleton, Table, Typography } from 'antd'; -import type { ColumnsType } from 'antd/es/table'; -import getIngestionData from 'api/settings/getIngestionData'; -import { useQuery } from 'react-query'; +import { TagsFilled } from '@ant-design/icons'; +import { Color } from '@signozhq/design-tokens'; +import { + Button, + Col, + Collapse, + Form, + Input, + Modal, + Row, + Select, + Table, + TableProps, + 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 updateIngestionKey from 'api/IngestionKeys/updateIngestionKey'; +import axios, { AxiosError } from 'axios'; +import Tags from 'components/Tags/Tags'; +import { SOMETHING_WENT_WRONG } from 'constants/api'; +import dayjs from 'dayjs'; +import { useGetAllIngestionsKeys } from 'hooks/IngestionKeys/useGetAllIngestionKeys'; +import { useNotifications } from 'hooks/useNotifications'; +import { Check, Copy, PenLine, Plus, Search, Trash2, X } from 'lucide-react'; +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 { IngestionDataType } from 'types/api/settings/ingestion'; +import { IngestionKeyProps } from 'types/api/ingestionKeys/types'; import AppReducer from 'types/reducer/app'; +import { USER_ROLES } from 'types/roles'; -export default function IngestionSettings(): JSX.Element { +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; +}; + +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 IngestionSettings(): JSX.Element { const { user } = useSelector((state) => state.app); + const { notifications } = useNotifications(); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [isAddModalOpen, setIsAddModalOpen] = useState(false); + const [showNewAPIKeyDetails, setShowNewAPIKeyDetails] = useState(false); + const [, handleCopyToClipboard] = useCopyToClipboard(); - const { data: ingestionData, isFetching } = useQuery({ - queryFn: getIngestionData, - queryKey: ['getIngestionData', user?.userId], - }); + const [updatedTags, setUpdatedTags] = useState([]); - const columns: ColumnsType = [ - { - title: 'Name', - dataIndex: 'name', - key: 'name', - render: (text): JSX.Element => {text} , + const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const [activeAPIKey, setActiveAPIKey] = useState(); + + const [searchValue, setSearchValue] = useState(''); + const [dataSource, setDataSource] = useState([]); + const { t } = useTranslation(['ingestionKeys']); + + 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: IngestionKeyProps): 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: IngestionKeyProps): void => { + handleFormReset(); + setActiveAPIKey(apiKey); + + editForm.setFieldsValue({ + name: apiKey.name, + }); + + setUpdatedTags(apiKey.tags || []); + + setIsEditModalOpen(true); + }; + + const showAddModal = (): void => { + setUpdatedTags([]); + setActiveAPIKey(null); + setIsAddModalOpen(true); + }; + + const handleModalClose = (): void => { + setActiveAPIKey(null); + }; + + const { + data: IngestionKeys, + isLoading, + isRefetching, + refetch: refetchAPIKeys, + error, + isError, + } = useGetAllIngestionsKeys(); + + useEffect(() => { + setActiveAPIKey(IngestionKeys?.data.data[0]); + }, [IngestionKeys]); + + useEffect(() => { + setDataSource(IngestionKeys?.data.data || []); + }, [IngestionKeys?.data.data]); + + useEffect(() => { + if (isError) { + showErrorNotification(notifications, error as AxiosError); + } + }, [error, isError, notifications]); + + const handleSearch = (e: ChangeEvent): void => { + setSearchValue(e.target.value); + const filteredData = IngestionKeys?.data?.data?.filter( + (key: IngestionKeyProps) => + key && + key.name && + key.name.toLowerCase().includes(e.target.value.toLowerCase()), + ); + setDataSource(filteredData || []); + }; + + const clearSearch = (): void => { + setSearchValue(''); + }; + + const { + mutate: createIngestionKey, + isLoading: isLoadingCreateAPIKey, + } = useMutation(createIngestionKeyApi, { + onSuccess: (data) => { + setShowNewAPIKeyDetails(true); + setActiveAPIKey(data.payload); + + refetchAPIKeys(); }, - { - title: '', - dataIndex: 'value', - key: 'value', - render: (text): JSX.Element => ( -
- {isFetching ? ( - - ) : ( - - {text} - - )} -
- ), + onError: (error) => { + showErrorNotification(notifications, error as AxiosError); }, - ]; - - const injectionDataPayload = - ingestionData && - ingestionData.payload && - Array.isArray(ingestionData.payload) && - ingestionData?.payload[0]; + }); - const data: IngestionDataType[] = [ + const { mutate: updateAPIKey, isLoading: isLoadingUpdateAPIKey } = useMutation( + updateIngestionKey, { - key: '1', - name: 'Ingestion URL', - value: injectionDataPayload?.ingestionURL, + onSuccess: () => { + refetchAPIKeys(); + setIsEditModalOpen(false); + }, + onError: (error) => { + showErrorNotification(notifications, error as AxiosError); + }, }, + ); + + const { mutate: deleteAPIKey, isLoading: isDeleteingAPIKey } = useMutation( + deleteIngestionKey, { - key: '2', - name: 'Ingestion Key', - value: injectionDataPayload?.ingestionKey, + 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, + tags: updatedTags, + }, + }); + } + }) + .catch((errorInfo) => { + console.error('error info', errorInfo); + }); + }; + + const onCreateIngestionKey = (): void => { + createForm + .validateFields() + .then((values) => { + if (user) { + const requestPayload = { + name: values.name, + tags: updatedTags, + }; + + // if(values.expries_at !== 0) { + // requestPayload.expires_at = + // } + + createIngestionKey(requestPayload); + setUpdatedTags([]); + } + }) + .catch((errorInfo) => { + console.error('error info', errorInfo); + }); + }; + + const handleCopyKey = (text: string): void => { + handleCopyToClipboard(text); + notifications.success({ + message: 'Copied to clipboard', + }); + }; + + const getFormattedTime = (date: string): string => + dayjs(date).format('MMM DD,YYYY, hh:mm a'); + + const handleCopyClose = (): void => { + if (activeAPIKey) { + handleCopyKey(activeAPIKey?.value); + } + + hideAddViewModal(); + }; + + const columns: TableProps['columns'] = [ { - key: '3', - name: 'Ingestion Region', - value: injectionDataPayload?.dataRegion, + title: 'Ingestion Key', + key: 'ingestion-key', + // eslint-disable-next-line sonarjs/cognitive-complexity + render: (APIKey: IngestionKeyProps): JSX.Element => { + const createdOn = getFormattedTime(APIKey.created_at); + + // const expiresIn = + // APIKey.expires_at === 0 + // ? Number.POSITIVE_INFINITY + // : getDateDifference(APIKey?.created_at, APIKey?.expires_at); + + // const isExpired = isExpiredToken(APIKey.expires_at); + + const expiresOn = APIKey.expires_at + ? getFormattedTime(APIKey.expires_at) + : 'No Expiry'; + + const updatedOn = getFormattedTime(APIKey?.updated_at); + + 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.expires_at && ( + + Expires on + + {expiresOn} + + + )} +
+ ), + }, + ]; + + return ( +
+ + +
+
+ + +
+ {APIKey.tags && + Array.isArray(APIKey.tags) && + APIKey.tags.length > 0 && + APIKey.tags.map((tag, index) => ( + // eslint-disable-next-line react/no-array-index-key + {tag} + ))} +
+
+
+
+ ); + }, }, ]; return ( -
- +
+
+ Ingestion Keys + + Create and manage ingestion keys for the SigNoz Cloud + +
+ +
+ } + value={searchValue} + onChange={handleSearch} + /> + + +
+ + + `${range[0]}-${range[1]} of ${total} Ingestion keys`, + }} + /> + + + {/* Delete Key Modal */} + Delete Ingestion Key} + open={isDeleteModalOpen} + closable + afterClose={handleModalClose} + onCancel={hideDeleteViewModal} + destroyOnClose + footer={[ + , + , + ]} > - You can use the following ingestion credentials to start sending your - telemetry data to SigNoz - - -
+ + {t('delete_confirm_message', { + keyName: activeAPIKey?.name, + })} + + + + {/* Edit Key Modal */} + } + > + Cancel + , + , + ]} + > +
+ + + + + + + + + + + + + + + + + + + - - + + - - - - - - - - - - + } + onBlur={(e) => handleBlur(limit.id, 'size', parseInt(e.target.value))} + /> + + +
OR
+
+ handleBlur(limit.id, 'count', parseInt(e.target.value))} + /> +
+ + + ); + })} + + ); +} diff --git a/frontend/src/periscope.scss b/frontend/src/periscope.scss index 423776d4aa8..097f22f7c1c 100644 --- a/frontend/src/periscope.scss +++ b/frontend/src/periscope.scss @@ -28,7 +28,8 @@ &.primary { color: #fff; - background-color: #4566d6; + background-color: var(--bg-robin-500, #4E74F8); + border: none; box-shadow: 0 2px 0 rgba(62, 86, 245, 0.09); } @@ -49,4 +50,4 @@ background: var(--bg-vanilla-100); color: var(--bg-ink-200); } -} +} \ No newline at end of file diff --git a/frontend/src/periscope/components/Tabs/Tabs.tsx b/frontend/src/periscope/components/Tabs/Tabs.tsx new file mode 100644 index 00000000000..9b4536df84f --- /dev/null +++ b/frontend/src/periscope/components/Tabs/Tabs.tsx @@ -0,0 +1,24 @@ +import { Tabs as AntDTabs } from 'antd'; + +export interface TabProps { + label: string | React.ReactElement + key: string + children: React.ReactElement +} + + +export interface TabsProps { + items: TabProps[] +} + +export default function Tabs({ + items +}: TabsProps) { + return ( + + ) +} diff --git a/frontend/src/periscope/components/Tabs/index.tsx b/frontend/src/periscope/components/Tabs/index.tsx new file mode 100644 index 00000000000..e0629d98456 --- /dev/null +++ b/frontend/src/periscope/components/Tabs/index.tsx @@ -0,0 +1,3 @@ +import Tabs from './Tabs'; + +export default Tabs; \ No newline at end of file diff --git a/frontend/src/types/api/ingestionKeys/types.ts b/frontend/src/types/api/ingestionKeys/types.ts index d7f6d05a2a6..66481fb1eba 100644 --- a/frontend/src/types/api/ingestionKeys/types.ts +++ b/frontend/src/types/api/ingestionKeys/types.ts @@ -21,7 +21,7 @@ export interface IngestionKeyProps { export interface CreateIngestionKeyProps { name: string; - expires_at?: number; + expires_at: string; tags: string[]; } diff --git a/frontend/src/utils/app.ts b/frontend/src/utils/app.ts index fdc95ee471f..fa486768357 100644 --- a/frontend/src/utils/app.ts +++ b/frontend/src/utils/app.ts @@ -15,6 +15,8 @@ export function extractDomain(email: string): string { export const isCloudUser = (): boolean => { const { hostname } = window.location; + return true; + return hostname?.endsWith('signoz.cloud'); }; diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 00000000000..fb57ccd13af --- /dev/null +++ b/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + From 227fb58e26667a74089f225d83912b30eebc7ede Mon Sep 17 00:00:00 2001 From: Yunus A M Date: Fri, 31 May 2024 17:04:55 +0530 Subject: [PATCH 04/14] feat: handle limit updates from component --- .../IngestionSettings/IngestionKeyDetails.tsx | 38 +++++++++++++-- .../IngestionSettings/Limits/Limits.tsx | 48 ++++++++++++------- .../src/periscope/components/Tabs/Tabs.tsx | 25 +++------- 3 files changed, 70 insertions(+), 41 deletions(-) diff --git a/frontend/src/container/IngestionSettings/IngestionKeyDetails.tsx b/frontend/src/container/IngestionSettings/IngestionKeyDetails.tsx index f76db6cfc3b..0044eb55b94 100644 --- a/frontend/src/container/IngestionSettings/IngestionKeyDetails.tsx +++ b/frontend/src/container/IngestionSettings/IngestionKeyDetails.tsx @@ -44,7 +44,7 @@ import Tags from 'components/Tags/Tags'; import { InfoCircleFilled } from '@ant-design/icons'; import Tabs from 'periscope/components/Tabs'; import { TabProps } from 'periscope/components/Tabs/Tabs'; -import Limits from './Limits/Limits'; +import Limits, { LimitProps } from './Limits/Limits'; import dayjs from 'dayjs'; interface IngestionKeyDetailsProps { @@ -69,7 +69,12 @@ function IngestionKeyDetails({ const [, copyToClipboard] = useCopyToClipboard(); const [editForm] = Form.useForm(); const isDarkMode = useIsDarkMode(); - const [first, setfirst] = useState(); + + const [signalLimits, setSignalLimits] = useState({ + logs: {}, + metrics: {}, + traces: {}, + }); const drawerCloseHandler = ( e: React.MouseEvent | React.KeyboardEvent, @@ -79,7 +84,21 @@ function IngestionKeyDetails({ } }; - const handleLimitUpdate = (id, values) => {}; + const handleLimitUpdate = ( + id: string, + values: { + [key: string]: LimitProps; + }, + ) => { + setSignalLimits((prevState) => ({ + ...prevState, + [id]: values, + })); + }; + + const handleTabChange = (activeKey: string) => { + console.log('tab change', activeKey); + }; const tabItems: TabProps[] = [ { @@ -115,7 +134,11 @@ function IngestionKeyDetails({ editForm .validateFields() .then((values) => { - onUpdateIngestionKeyDetails({ ...values, tags: updatedTags }); + onUpdateIngestionKeyDetails({ + ...values, + tags: updatedTags, + limits: signalLimits, + }); }) .catch((errorInfo) => { console.error('error info', errorInfo); @@ -219,7 +242,12 @@ function IngestionKeyDetails({
- +
diff --git a/frontend/src/container/IngestionSettings/Limits/Limits.tsx b/frontend/src/container/IngestionSettings/Limits/Limits.tsx index a7989ebdec9..7fdf02f3bc4 100644 --- a/frontend/src/container/IngestionSettings/Limits/Limits.tsx +++ b/frontend/src/container/IngestionSettings/Limits/Limits.tsx @@ -38,33 +38,35 @@ const LIMITS = [ }, ]; +export interface LimitProps { + size: number; + count: number; + sizeUnit: string; + enabled: boolean; +} + interface LimitsProps { id: string; - onLimitUpdate: (id: string, data: Record) => void; + onLimitUpdate: ( + id: string, + data: { + [key: string]: LimitProps; + }, + ) => void; } export default function Limits({ id, onLimitUpdate }: LimitsProps) { const [limitValues, setLimitValues] = useState<{ - [key: string]: { - size: number; - count: number; - sizeUnit: string; - enabled: boolean; - }; + [key: string]: LimitProps; }>(() => { const initialLimits: { - [key: string]: { - size: number; - count: number; - sizeUnit: string; - enabled: boolean; - }; + [key: string]: LimitProps; } = {}; LIMITS.forEach((limit) => { initialLimits[limit.id] = { - size: 10, - count: 10, + size: 0, + count: 0, sizeUnit: 'GB', enabled: false, }; @@ -139,7 +141,13 @@ export default function Limits({ id, onLimitUpdate }: LimitsProps) { } - onBlur={(e) => handleBlur(limit.id, 'size', parseInt(e.target.value))} + onBlur={(e) => + handleBlur( + limit.id, + 'size', + e.target.value ? parseInt(e.target.value) : 0, + ) + } /> @@ -149,7 +157,13 @@ export default function Limits({ id, onLimitUpdate }: LimitsProps) { addonAfter="Spans" disabled={!limitValues[limit.id].enabled} min={1} - onBlur={(e) => handleBlur(limit.id, 'count', parseInt(e.target.value))} + onBlur={(e) => + handleBlur( + limit.id, + 'count', + e.target.value ? parseInt(e.target.value) : 0, + ) + } /> diff --git a/frontend/src/periscope/components/Tabs/Tabs.tsx b/frontend/src/periscope/components/Tabs/Tabs.tsx index 9b4536df84f..96eb2e50ad5 100644 --- a/frontend/src/periscope/components/Tabs/Tabs.tsx +++ b/frontend/src/periscope/components/Tabs/Tabs.tsx @@ -1,24 +1,11 @@ -import { Tabs as AntDTabs } from 'antd'; +import { Tabs as AntDTabs, TabsProps } from 'antd'; export interface TabProps { - label: string | React.ReactElement - key: string - children: React.ReactElement + label: string | React.ReactElement; + key: string; + children: React.ReactElement; } - -export interface TabsProps { - items: TabProps[] -} - -export default function Tabs({ - items -}: TabsProps) { - return ( - - ) +export default function Tabs(props: TabsProps) { + return ; } From 72db16240bde9530962fd1c0e9a31ce96789b00b Mon Sep 17 00:00:00 2001 From: Yunus A M Date: Sun, 2 Jun 2024 17:22:09 +0530 Subject: [PATCH 05/14] feat: handle limit updates per signal --- .../IngestionSettings.styles.scss | 124 ++++++- .../IngestionSettings/IngestionSettings.tsx | 315 +++++++++++++++++- frontend/src/types/api/ingestionKeys/types.ts | 8 +- 3 files changed, 420 insertions(+), 27 deletions(-) diff --git a/frontend/src/container/IngestionSettings/IngestionSettings.styles.scss b/frontend/src/container/IngestionSettings/IngestionSettings.styles.scss index 87985cd6f01..a2122c10335 100644 --- a/frontend/src/container/IngestionSettings/IngestionSettings.styles.scss +++ b/frontend/src/container/IngestionSettings/IngestionSettings.styles.scss @@ -182,7 +182,6 @@ margin-left: 8px; } - .ingestion-key-last-used-at { display: flex; align-items: center; @@ -196,7 +195,8 @@ line-height: 18px; /* 128.571% */ letter-spacing: -0.07px; - font-variant-numeric: lining-nums tabular-nums stacked-fractions slashed-zero; + font-variant-numeric: lining-nums tabular-nums stacked-fractions + slashed-zero; font-feature-settings: 'dlig' on, 'salt' on, 'cpsp' on, 'case' on; } } @@ -243,7 +243,7 @@ justify-content: center; align-items: center; - >a { + > a { color: var(--bg-vanilla-400); font-variant-numeric: lining-nums tabular-nums slashed-zero; font-feature-settings: 'dlig' on, 'salt' on, 'case' on, 'cpsp' on; @@ -258,7 +258,7 @@ .ant-pagination-item-active { background-color: var(--bg-robin-500); - >a { + > a { color: var(--bg-ink-500) !important; font-size: var(--font-size-sm); font-style: normal; @@ -308,6 +308,101 @@ align-items: center; gap: 16px; } + + .limits-data { + padding: 16px; + border: 1px solid var(--bg-slate-500); + + .signals { + .signal { + margin-bottom: 24px; + + .header { + display: flex; + justify-content: space-between; + align-items: center; + + .actions { + display: flex; + align-items: center; + gap: 4px; + } + } + + .signal-name { + font-size: 12px; + font-weight: 500; + text-transform: uppercase; + + color: var(--bg-robin-500); + } + + .signal-limit-values { + display: flex; + gap: 16px; + margin-top: 8px; + margin-bottom: 16px; + + .ant-form-item { + margin-bottom: 12px; + } + + .daily-limit, + .second-limit { + flex: 1; + + .heading { + .title { + font-size: 12px; + } + + .subtitle { + font-size: 11px; + } + + padding: 4px 0px; + } + + .ant-input-number { + width: 80%; + } + } + + .signal-limit-view-mode { + display: flex; + width: 100%; + justify-content: space-between; + gap: 16px; + + .signal-limit-value { + display: flex; + align-items: center; + gap: 8px; + + flex: 1; + + .limit-type { + display: flex; + align-items: center; + gap: 8px; + } + } + } + + .signal-limit-edit-mode { + display: flex; + justify-content: space-between; + gap: 16px; + } + } + + .signal-limit-save-discard { + display: flex; + gap: 8px; + } + } + } + } } .ingestion-key-modal { @@ -536,7 +631,6 @@ border: 1px solid var(--Slate-500, #161922); } - #edit-ingestion-key-form { .ant-form-item:last-child { margin-bottom: 0px; @@ -551,8 +645,8 @@ margin: 16px 0; border-radius: 4px; - background: rgba(113, 144, 249, 0.10); - color: var(--Robin-300, #95ACFB); + background: rgba(113, 144, 249, 0.1); + color: var(--Robin-300, #95acfb); font-size: 13px; font-style: normal; font-weight: 400; @@ -584,7 +678,6 @@ border-radius: 3px; border: 1px solid var(--Slate-500, #161922); - .ant-tabs { .ant-tabs-nav { margin-top: -36px; @@ -596,9 +689,9 @@ .ant-tabs-nav-list { background: #121317; border-radius: 2px; - border: 1px solid var(--Slate-400, #1D212D); + border: 1px solid var(--Slate-400, #1d212d); background: var(--Ink-400, #121317); - box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.10); + box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1); .ant-tabs-tab { display: inline-flex; @@ -606,13 +699,12 @@ justify-content: center; align-items: center; margin: 0px; - border-right: 1px solid #1D212D; + border-right: 1px solid #1d212d; &.ant-tabs-tab-active { border-bottom: 0px; } - .tab-name { display: flex; align-items: center; @@ -626,9 +718,9 @@ .ingestion-key-expires-at { border-radius: 4px; - border: 1px solid var(--bg-slate-500); - background: var(--bg-ink-400); - box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2); + border: 1px solid var(--bg-slate-500); + background: var(--bg-ink-400); + box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2); } .lightMode { @@ -815,4 +907,4 @@ .copyable-text { background: var(--bg-vanilla-200); } -} \ No newline at end of file +} diff --git a/frontend/src/container/IngestionSettings/IngestionSettings.tsx b/frontend/src/container/IngestionSettings/IngestionSettings.tsx index 65b3efded02..15155752f51 100644 --- a/frontend/src/container/IngestionSettings/IngestionSettings.tsx +++ b/frontend/src/container/IngestionSettings/IngestionSettings.tsx @@ -9,6 +9,7 @@ import { DatePicker, Form, Input, + InputNumber, Modal, Row, Select, @@ -35,7 +36,9 @@ import { Minus, PenLine, Plus, + PlusIcon, Search, + Tally1, Trash2, X, } from 'lucide-react'; @@ -45,10 +48,11 @@ import { useMutation } from 'react-query'; import { useSelector } from 'react-redux'; import { useCopyToClipboard } from 'react-use'; import { AppState } from 'store/reducers'; -import { IngestionKeyProps } from 'types/api/ingestionKeys/types'; +import { IngestionKeyProps, Limit } from 'types/api/ingestionKeys/types'; import AppReducer from 'types/reducer/app'; import { USER_ROLES } from 'types/roles'; -import IngestionKeyDetails from './IngestionKeyDetails'; + +const { Option } = Select; import cx from 'classnames'; import { @@ -62,6 +66,8 @@ export const disabledDate = (current) => { return current && current < dayjs().endOf('day'); }; +const SIGNALS = ['logs', 'traces', 'metrics']; + export const showErrorNotification = ( notifications: NotificationInstance, err: Error, @@ -91,10 +97,10 @@ function IngestionSettings(): JSX.Element { const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [, handleCopyToClipboard] = useCopyToClipboard(); - const [updatedTags, setUpdatedTags] = useState([]); - const [openEditMode, setOpenEditMode] = useState(false); + const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [activeAPIKey, setActiveAPIKey] = useState(); + const [activeSignal, setActiveSignal] = useState(null); const [searchValue, setSearchValue] = useState(''); const [dataSource, setDataSource] = useState([]); @@ -122,7 +128,7 @@ function IngestionSettings(): JSX.Element { const hideEditViewModal = (): void => { handleFormReset(); setActiveAPIKey(null); - setOpenEditMode(false); + setIsEditModalOpen(false); }; const hideAddViewModal = (): void => { @@ -135,7 +141,7 @@ function IngestionSettings(): JSX.Element { handleFormReset(); setActiveAPIKey(apiKey); setUpdatedTags(apiKey.tags || []); - setOpenEditMode(true); + setIsEditModalOpen(true); }; const showAddModal = (): void => { @@ -206,7 +212,7 @@ function IngestionSettings(): JSX.Element { { onSuccess: () => { refetchAPIKeys(); - setOpenEditMode(true); + setIsEditModalOpen(false); }, onError: (error) => { showErrorNotification(notifications, error as AxiosError); @@ -295,6 +301,32 @@ function IngestionSettings(): JSX.Element { console.log('data', data); }; + const handleAddLimit = (APIKey: IngestionKeyProps, signalName: string) => { + console.log('add - api key', APIKey); + console.log('signalName key', signalName); + + setActiveAPIKey(APIKey); + setActiveSignal(signalName); + }; + + const handleEditLimit = (APIKey: IngestionKeyProps, signalName: string) => { + console.log('edit - api key', APIKey); + console.log('signalName key', signalName); + + setActiveAPIKey(APIKey); + setActiveSignal(signalName); + }; + + const handleDeleteLimit = (APIKey: IngestionKeyProps, signalName: string) => { + console.log('delete - api key', APIKey); + console.log('signalName key', signalName); + }; + + const handleDiscardSaveLimit = () => { + setActiveAPIKey(null); + setActiveSignal(null); + }; + const columns: TableProps['columns'] = [ { title: 'Ingestion Key', @@ -302,7 +334,6 @@ function IngestionSettings(): JSX.Element { // eslint-disable-next-line sonarjs/cognitive-complexity render: (APIKey: IngestionKeyProps): JSX.Element => { const createdOn = getFormattedTime(APIKey.created_at); - const formattedDateAndTime = APIKey && APIKey?.expires_at && getFormattedTime(APIKey?.expires_at); @@ -317,6 +348,20 @@ function IngestionSettings(): JSX.Element { const isExpired = isExpiredToken(dayjs(APIKey.expires_at).date()); + const hasLogsLimits = false; + const hasTracesLimits = false; + const hasMetricsLimits = false; + + const limits: { [key: string]: Limit } = {}; + + APIKey.limits?.forEach((limit: Limit) => (limits[limit.signal] = limit)); + + const hasLimits = (signal): boolean => { + return !!limits[signal]; + }; + + console.log('limits', limits); + const items: CollapseProps['items'] = [ { key: '1', @@ -375,6 +420,7 @@ function IngestionSettings(): JSX.Element { {createdOn} + {updatedOn && (
Updated on @@ -399,6 +445,188 @@ function IngestionSettings(): JSX.Element { )} + +
+

LIMITS

+ +
+
+ {SIGNALS.map((signal) => { + return ( +
+
+
{signal}
+
+ {hasLimits(signal) ? ( + <> + + )} +
+
+ +
+ {activeAPIKey?.id === APIKey.id && activeSignal === signal && ( +
+ +
+
Daily limit
+
+ Add a limit for data ingested daily{' '} +
+
+ +
+ + // handleBlur(limit.id, 'sizeUnit', value) + // } + // disabled={!limitValues[limit.id].enabled} + > + + + + + + } + /> +
+
+ + +
+
Per Second limit
+
+ {' '} + Add a limit for data ingested every second{' '} +
+
+ +
+ + // handleBlur(limit.id, 'sizeUnit', value) + // } + // disabled={!limitValues[limit.id].enabled} + > + + + + + + } + /> +
+
+
+ )} + + {(activeAPIKey?.id !== APIKey.id || + !activeSignal || + activeSignal !== signal) && ( +
+
+
+ Daily {' '} +
+ +
2.6 GB / 4 GB
+
+ +
+
+ Seconds +
+ +
2.6 GB / 4 GB
+
+
+ )} +
+ + {activeAPIKey?.id === APIKey.id && activeSignal === signal && ( +
+ + +
+ )} +
+ ); + })} +
+
+
), }, @@ -498,7 +726,74 @@ function IngestionSettings(): JSX.Element { - {activeAPIKey && openEditMode && ( + {/* Edit Key Modal */} + } + > + Cancel + , + , + ]} + > + + + + + + + + + + + + + + + + {/* {activeAPIKey && openEditMode && ( hideEditViewModal()} openDrawer={openEditMode} @@ -508,7 +803,7 @@ function IngestionSettings(): JSX.Element { handleCopyKey={handleCopyKey} onUpdateIngestionKeyDetails={handleIngestionKeyDataUpdate} /> - )} + )} */} {/* Create New Key Modal */} , + tags?: [] +} + export interface IngestionKeyProps { name: string; expires_at?: string; @@ -16,7 +22,7 @@ export interface IngestionKeyProps { created_at: string; updated_at: string; tags?: string[]; - limits?: string[]; + limits?: Limit[]; } export interface CreateIngestionKeyProps { From 098c365c26396344cbb1ecf25028c7cfe3e96a75 Mon Sep 17 00:00:00 2001 From: Yunus A M Date: Mon, 3 Jun 2024 09:26:43 +0530 Subject: [PATCH 06/14] feat: integrate multiple ingestion key api --- .../limits/createLimitsForKey.ts | 67 +++ .../limits/deleteLimitsForIngestionKey.ts | 26 + .../limits/updateLimitsForIngestionKey.ts | 67 +++ .../IngestionSettings.styles.scss | 18 + .../IngestionSettings/IngestionSettings.tsx | 509 +++++++++++------- .../IngestionSettings/Limits/Limits.tsx | 2 +- .../types/api/ingestionKeys/limits/types.ts | 17 + frontend/src/types/api/ingestionKeys/types.ts | 13 +- 8 files changed, 507 insertions(+), 212 deletions(-) create mode 100644 frontend/src/api/IngestionKeys/limits/createLimitsForKey.ts create mode 100644 frontend/src/api/IngestionKeys/limits/deleteLimitsForIngestionKey.ts create mode 100644 frontend/src/api/IngestionKeys/limits/updateLimitsForIngestionKey.ts create mode 100644 frontend/src/types/api/ingestionKeys/limits/types.ts diff --git a/frontend/src/api/IngestionKeys/limits/createLimitsForKey.ts b/frontend/src/api/IngestionKeys/limits/createLimitsForKey.ts new file mode 100644 index 00000000000..d02c190334b --- /dev/null +++ b/frontend/src/api/IngestionKeys/limits/createLimitsForKey.ts @@ -0,0 +1,67 @@ +/* eslint-disable @typescript-eslint/no-throw-literal */ +import { GatewayApiV1Instance } from 'api'; +import axios, { AxiosError } from 'axios'; +import { + LimitProps, + LimitSuccessProps, +} from 'types/api/ingestionKeys/limits/types'; + +interface SuccessResponse { + statusCode: number; + error: null; + message: string; + payload: T; +} + +interface ErrorResponse { + statusCode: number; + error: string; + message: string; + payload: null; +} + +const createLimitForIngestionKey = async ( + props: LimitProps, +): Promise | AxiosError> => { + try { + const response = await GatewayApiV1Instance.post( + `/workspaces/me/keys/${props.keyId}/limits`, + { + ...props, + }, + ); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; + } catch (error) { + if (axios.isAxiosError(error)) { + console.log('error', error); + + // Axios error + const errResponse: ErrorResponse = { + statusCode: error.response?.status || 500, + error: error.response?.data?.error, + message: error.response?.data?.status || 'An error occurred', + payload: null, + }; + + throw errResponse; + } else { + // Non-Axios error + const errResponse: ErrorResponse = { + statusCode: 500, + error: 'Unknown error', + message: 'An unknown error occurred', + payload: null, + }; + + throw errResponse; + } + } +}; + +export default createLimitForIngestionKey; diff --git a/frontend/src/api/IngestionKeys/limits/deleteLimitsForIngestionKey.ts b/frontend/src/api/IngestionKeys/limits/deleteLimitsForIngestionKey.ts new file mode 100644 index 00000000000..c0b3480c456 --- /dev/null +++ b/frontend/src/api/IngestionKeys/limits/deleteLimitsForIngestionKey.ts @@ -0,0 +1,26 @@ +import { GatewayApiV1Instance } from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { AllIngestionKeyProps } from 'types/api/ingestionKeys/types'; + +const deleteLimitsForIngestionKey = async ( + id: string, +): Promise | ErrorResponse> => { + try { + const response = await GatewayApiV1Instance.delete( + `/workspaces/me/limits/${id}`, + ); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default deleteLimitsForIngestionKey; diff --git a/frontend/src/api/IngestionKeys/limits/updateLimitsForIngestionKey.ts b/frontend/src/api/IngestionKeys/limits/updateLimitsForIngestionKey.ts new file mode 100644 index 00000000000..bb776e8c83d --- /dev/null +++ b/frontend/src/api/IngestionKeys/limits/updateLimitsForIngestionKey.ts @@ -0,0 +1,67 @@ +/* eslint-disable @typescript-eslint/no-throw-literal */ +import { GatewayApiV1Instance } from 'api'; +import axios, { AxiosError } from 'axios'; +import { + LimitProps, + LimitSuccessProps, +} from 'types/api/ingestionKeys/limits/types'; + +interface SuccessResponse { + statusCode: number; + error: null; + message: string; + payload: T; +} + +interface ErrorResponse { + statusCode: number; + error: string; + message: string; + payload: null; +} + +const updateLimitForIngestionKey = async ( + props: LimitProps, +): Promise | AxiosError> => { + try { + const response = await GatewayApiV1Instance.post( + `/workspaces/me/keys/${props.keyId}/limits`, + { + ...props, + }, + ); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; + } catch (error) { + if (axios.isAxiosError(error)) { + console.log('error', error); + + // Axios error + const errResponse: ErrorResponse = { + statusCode: error.response?.status || 500, + error: error.response?.data?.error, + message: error.response?.data?.status || 'An error occurred', + payload: null, + }; + + throw errResponse; + } else { + // Non-Axios error + const errResponse: ErrorResponse = { + statusCode: 500, + error: 'Unknown error', + message: 'An unknown error occurred', + payload: null, + }; + + throw errResponse; + } + } +}; + +export default updateLimitForIngestionKey; diff --git a/frontend/src/container/IngestionSettings/IngestionSettings.styles.scss b/frontend/src/container/IngestionSettings/IngestionSettings.styles.scss index a2122c10335..60eb8cd027b 100644 --- a/frontend/src/container/IngestionSettings/IngestionSettings.styles.scss +++ b/frontend/src/container/IngestionSettings/IngestionSettings.styles.scss @@ -343,6 +343,10 @@ margin-top: 8px; margin-bottom: 16px; + .edit-ingestion-key-limit-form { + width: 100%; + } + .ant-form-item { margin-bottom: 12px; } @@ -386,6 +390,15 @@ align-items: center; gap: 8px; } + + .limit-value { + display: flex; + align-items: center; + gap: 8px; + + font-size: 12px; + font-weight: 600; + } } } @@ -654,6 +667,11 @@ letter-spacing: 0.013px; } +.error { + color: var(--bg-cherry-500); + margin-bottom: 8px; +} + .save-discard-changes { display: flex; gap: 8px; diff --git a/frontend/src/container/IngestionSettings/IngestionSettings.tsx b/frontend/src/container/IngestionSettings/IngestionSettings.tsx index 15155752f51..532ec0ca3dd 100644 --- a/frontend/src/container/IngestionSettings/IngestionSettings.tsx +++ b/frontend/src/container/IngestionSettings/IngestionSettings.tsx @@ -1,6 +1,5 @@ import './IngestionSettings.styles.scss'; -import { TagsFilled } from '@ant-design/icons'; import { Color } from '@signozhq/design-tokens'; import { Button, @@ -9,7 +8,6 @@ import { DatePicker, Form, Input, - InputNumber, Modal, Row, Select, @@ -22,23 +20,26 @@ 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 deleteLimitsForIngestionKey from 'api/IngestionKeys/limits/deleteLimitsForIngestionKey'; +import createLimitForIngestionKeyApi from 'api/IngestionKeys/limits/createLimitsForKey'; import updateIngestionKey from 'api/IngestionKeys/updateIngestionKey'; -import axios, { AxiosError } from 'axios'; +import updateLimitForIngestionKey from 'api/IngestionKeys/limits/updateLimitsForIngestionKey'; +import { AxiosError } from 'axios'; import Tags from 'components/Tags/Tags'; import { SOMETHING_WENT_WRONG } from 'constants/api'; -import dayjs from 'dayjs'; +import dayjs, { Dayjs } from 'dayjs'; import { useGetAllIngestionsKeys } from 'hooks/IngestionKeys/useGetAllIngestionKeys'; import { useNotifications } from 'hooks/useNotifications'; import { CalendarClock, Check, Copy, + Infinity, Minus, PenLine, Plus, PlusIcon, Search, - Tally1, Trash2, X, } from 'lucide-react'; @@ -51,20 +52,15 @@ import { AppState } from 'store/reducers'; import { IngestionKeyProps, Limit } from 'types/api/ingestionKeys/types'; import AppReducer from 'types/reducer/app'; import { USER_ROLES } from 'types/roles'; +import { LimitProps, LimitsProps } from './Limits/Limits'; const { Option } = Select; -import cx from 'classnames'; -import { - EXPIRATION_WITHIN_SEVEN_DAYS, - getDateDifference, - isExpiredToken, -} from 'container/APIKeys/APIKeys'; +const BYTES = 1073741824; -export const disabledDate = (current) => { +export const disabledDate = (current: Dayjs): boolean => // Disable all dates before today - return current && current < dayjs().endOf('day'); -}; + current && current < dayjs().endOf('day'); const SIGNALS = ['logs', 'traces', 'metrics']; @@ -73,7 +69,7 @@ export const showErrorNotification = ( err: Error, ): void => { notifications.error({ - message: axios.isAxiosError(err) ? err.message : SOMETHING_WENT_WRONG, + message: err.message || SOMETHING_WENT_WRONG, }); }; @@ -104,20 +100,30 @@ function IngestionSettings(): JSX.Element { const [searchValue, setSearchValue] = useState(''); const [dataSource, setDataSource] = useState([]); + + const [ + hasCreateLimitForIngestionKeyError, + setHasCreateLimitForIngestionKeyError, + ] = useState(false); 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 => { - handleFormReset(); - setActiveAPIKey(null); + console.log('hide delete modal'); setIsDeleteModalOpen(false); + setActiveAPIKey(null); + handleFormReset(); + + console.log('hide delete modal', activeAPIKey); }; const showDeleteModal = (apiKey: IngestionKeyProps): void => { @@ -126,9 +132,13 @@ function IngestionSettings(): JSX.Element { }; const hideEditViewModal = (): void => { - handleFormReset(); + console.log('hide edit modal'); + setActiveAPIKey(null); setIsEditModalOpen(false); + handleFormReset(); + + console.log('hide edit modal', activeAPIKey); }; const hideAddViewModal = (): void => { @@ -138,8 +148,11 @@ function IngestionSettings(): JSX.Element { }; const showEditModal = (apiKey: IngestionKeyProps): void => { - handleFormReset(); + console.log('show edit modal', apiKey); + setActiveAPIKey(apiKey); + + handleFormReset(); setUpdatedTags(apiKey.tags || []); setIsEditModalOpen(true); }; @@ -151,7 +164,9 @@ function IngestionSettings(): JSX.Element { }; const handleModalClose = (): void => { + console.log('modal close', activeAPIKey); setActiveAPIKey(null); + setActiveSignal(null); }; const { @@ -233,6 +248,37 @@ function IngestionSettings(): JSX.Element { }, ); + const { + mutate: createLimitForIngestionKey, + isLoading: isLoadingLimitForKey, + error: createLimitForIngestionKeyError, + } = useMutation(createLimitForIngestionKeyApi, { + onSuccess: () => { + setUpdatedTags([]); + hideAddViewModal(); + refetchAPIKeys(); + setHasCreateLimitForIngestionKeyError(false); + }, + onError: (error) => { + console.log('on error', error); + setHasCreateLimitForIngestionKeyError(true); + // showErrorNotification(notifications, error as AxiosError); + }, + }); + + const { mutate: deleteLimitForKey, isLoading: isDeletingLimit } = useMutation( + deleteLimitsForIngestionKey, + { + onSuccess: () => { + refetchAPIKeys(); + setIsDeleteModalOpen(false); + }, + onError: (error) => { + showErrorNotification(notifications, error as AxiosError); + }, + }, + ); + const onDeleteHandler = (): void => { clearSearch(); @@ -289,42 +335,76 @@ function IngestionSettings(): JSX.Element { const getFormattedTime = (date: string): string => dayjs(date).format('MMM DD,YYYY, hh:mm a'); - const handleCopyClose = (): void => { - if (activeAPIKey) { - handleCopyKey(activeAPIKey?.value); - } + const handleAddLimit = ( + APIKey: IngestionKeyProps, + signalName: string, + ): void => { + setActiveSignal(signalName); - hideAddViewModal(); - }; + const { dailyLimit, secondsLimit } = addEditLimitForm.getFieldsValue(); + + const payload = { + keyId: APIKey.id, + signal: signalName, + config: { + day: { + size: parseInt(dailyLimit, 10) * BYTES, + }, + second: { + size: parseInt(secondsLimit, 10) * BYTES, + }, + }, + }; - const handleIngestionKeyDataUpdate = (data) => { - console.log('data', data); + createLimitForIngestionKey(payload); }; - const handleAddLimit = (APIKey: IngestionKeyProps, signalName: string) => { - console.log('add - api key', APIKey); - console.log('signalName key', signalName); - + const enableEditLimitMode = ( + APIKey: IngestionKeyProps, + signalName: string, + ): void => { setActiveAPIKey(APIKey); setActiveSignal(signalName); }; - const handleEditLimit = (APIKey: IngestionKeyProps, signalName: string) => { - console.log('edit - api key', APIKey); - console.log('signalName key', signalName); - - setActiveAPIKey(APIKey); - setActiveSignal(signalName); - }; - - const handleDeleteLimit = (APIKey: IngestionKeyProps, signalName: string) => { + const handleDeleteLimit = ( + APIKey: IngestionKeyProps, + signal: LimitsProps, + ): void => { console.log('delete - api key', APIKey); - console.log('signalName key', signalName); + console.log('signalName key', signal); + + if (signal) { + deleteLimitForKey(signal.id); + } }; - const handleDiscardSaveLimit = () => { + const handleDiscardSaveLimit = (): void => { + setHasCreateLimitForIngestionKeyError(false); setActiveAPIKey(null); setActiveSignal(null); + + addEditLimitForm.resetFields(); + }; + + const handleSaveLimit = ( + APIKey: IngestionKeyProps, + signal: string, + addOrUpdate: string, + ): void => { + if (addOrUpdate === 'add') { + handleAddLimit(APIKey, signal); + } else { + // handleAddLimit(APIKey, signal); + } + }; + + const getFormattedLimit = (size: number | undefined): number => { + if (!size) { + return 1; + } + + return size / BYTES; }; const columns: TableProps['columns'] = [ @@ -337,28 +417,15 @@ function IngestionSettings(): JSX.Element { const formattedDateAndTime = APIKey && APIKey?.expires_at && getFormattedTime(APIKey?.expires_at); - const expiresIn = getDateDifference( - dayjs(APIKey?.created_at).date(), - dayjs(APIKey?.expires_at).date(), - ); - - const expiresOn = getFormattedTime(dayjs(APIKey.expires_at).toString()); - const updatedOn = getFormattedTime(APIKey?.updated_at); - const isExpired = isExpiredToken(dayjs(APIKey.expires_at).date()); - - const hasLogsLimits = false; - const hasTracesLimits = false; - const hasMetricsLimits = false; - const limits: { [key: string]: Limit } = {}; - APIKey.limits?.forEach((limit: Limit) => (limits[limit.signal] = limit)); + APIKey.limits?.forEach((limit: Limit) => { + limits[limit.signal] = limit; + }); - const hasLimits = (signal): boolean => { - return !!limits[signal]; - }; + const hasLimits = (signal: string): boolean => !!limits[signal]; console.log('limits', limits); @@ -396,6 +463,7 @@ function IngestionSettings(): JSX.Element { onClick={(e): void => { e.stopPropagation(); e.preventDefault(); + showEditModal(APIKey); }} /> @@ -447,66 +515,79 @@ function IngestionSettings(): JSX.Element { )}
-

LIMITS

+

LIMITS

- {SIGNALS.map((signal) => { - return ( -
-
-
{signal}
-
- {hasLimits(signal) ? ( - <> - - )} -
+ /> + + ) : ( + + )}
+
-
- {activeAPIKey?.id === APIKey.id && activeSignal === signal && ( +
+ {activeAPIKey?.id === APIKey.id && activeSignal === signal && ( +
- +
Daily limit
@@ -515,34 +596,24 @@ function IngestionSettings(): JSX.Element {
- - // handleBlur(limit.id, 'sizeUnit', value) - // } - // disabled={!limitValues[limit.id].enabled} - > - - - - - - } - /> + + + + + + + + } + /> +
- +
- +
Per Second limit
@@ -552,78 +623,109 @@ function IngestionSettings(): JSX.Element {
- - // handleBlur(limit.id, 'sizeUnit', value) - // } - // disabled={!limitValues[limit.id].enabled} - > - - - - - - } - /> + + + + + + + + } + /> +
- +
- )} - {(activeAPIKey?.id !== APIKey.id || - !activeSignal || - activeSignal !== signal) && ( -
-
-
- Daily {' '} + {activeAPIKey?.id === APIKey.id && + activeSignal === signal && + !isLoadingLimitForKey && + hasCreateLimitForIngestionKeyError && + createLimitForIngestionKeyError && + createLimitForIngestionKeyError?.error && ( +
+ {createLimitForIngestionKeyError?.error}
+ )} -
2.6 GB / 4 GB
+ {activeAPIKey?.id === APIKey.id && activeSignal === signal && ( +
+ +
+ )} + + )} -
-
- Seconds -
+ {(activeAPIKey?.id !== APIKey.id || + !activeSignal || + activeSignal !== signal) && ( +
+
+
+ Daily {' '} +
-
2.6 GB / 4 GB
+
+ {limits[signal]?.config?.day?.size ? ( + <> + {getFormattedLimit(limits[signal]?.config?.day?.size)} GB + + ) : ( + <> + NO LIMIT + + )}
- )} -
- {activeAPIKey?.id === APIKey.id && activeSignal === signal && ( -
- - +
+
+ Seconds +
+ +
+ {limits[signal]?.config?.day?.size ? ( + <> + {getFormattedLimit(limits[signal]?.config?.second?.size)} GB + + ) : ( + <> + NO LIMIT + + )} +
+
)}
- ); - })} +
+ ))}
@@ -726,15 +828,16 @@ function IngestionSettings(): JSX.Element { - {/* Edit Key Modal */} + {/* Edit Modal */}
- {/* {activeAPIKey && openEditMode && ( - hideEditViewModal()} - openDrawer={openEditMode} - data={activeAPIKey} - updatedTags={updatedTags} - onUpdatedTags={(tags) => setUpdatedTags(tags)} - handleCopyKey={handleCopyKey} - onUpdateIngestionKeyDetails={handleIngestionKeyDataUpdate} - /> - )} */} - {/* Create New Key Modal */} , - tags?: [] + signal: string; + config?: { + day?: { + size?: number; + }; + second?: { + size?: number; + }; + }; + tags?: []; } export interface IngestionKeyProps { From 550cce736d55aeb0429efe1e4b8c7c75d33b81f9 Mon Sep 17 00:00:00 2001 From: Yunus A M Date: Mon, 3 Jun 2024 16:33:25 +0530 Subject: [PATCH 07/14] feat: handle crud for limits --- frontend/public/locales/en/ingestionKeys.json | 3 +- .../api/IngestionKeys/createIngestionKey.ts | 2 - .../api/IngestionKeys/getAllIngestionKeys.ts | 14 +- .../limits/createLimitsForKey.ts | 12 +- .../limits/updateLimitsForIngestionKey.ts | 16 +- .../IngestionSettings/IngestionKeyDetails.tsx | 257 ------------- .../IngestionSettings/IngestionSettings.tsx | 345 +++++++++++++----- .../Limits/Limits.styles.scss | 61 ---- .../IngestionSettings/Limits/Limits.tsx | 175 --------- .../IngestionKeys/useGetAllIngestionKeys.ts | 16 +- .../types/api/ingestionKeys/limits/types.ts | 32 +- frontend/src/types/api/ingestionKeys/types.ts | 16 +- 12 files changed, 324 insertions(+), 625 deletions(-) delete mode 100644 frontend/src/container/IngestionSettings/IngestionKeyDetails.tsx delete mode 100644 frontend/src/container/IngestionSettings/Limits/Limits.styles.scss delete mode 100644 frontend/src/container/IngestionSettings/Limits/Limits.tsx diff --git a/frontend/public/locales/en/ingestionKeys.json b/frontend/public/locales/en/ingestionKeys.json index 256e88391a4..58ebf8a0d97 100644 --- a/frontend/public/locales/en/ingestionKeys.json +++ b/frontend/public/locales/en/ingestionKeys.json @@ -1,3 +1,4 @@ { - "delete_confirm_message": "Are you sure you want to delete {{keyName}}? Deleting an ingestion key is irreversible and cannot be undone." + "delete_confirm_message": "Are you sure you want to delete {{keyName}}? Deleting an ingestion key is irreversible and cannot be undone.", + "delete_limit_confirm_message": "Are you sure you want to delete {{limit_name}} limit for ingestion key {{keyName}}?" } diff --git a/frontend/src/api/IngestionKeys/createIngestionKey.ts b/frontend/src/api/IngestionKeys/createIngestionKey.ts index a2c474cdb1d..77556ed20ab 100644 --- a/frontend/src/api/IngestionKeys/createIngestionKey.ts +++ b/frontend/src/api/IngestionKeys/createIngestionKey.ts @@ -11,8 +11,6 @@ const createIngestionKey = async ( props: CreateIngestionKeyProps, ): Promise | ErrorResponse> => { try { - console.log('props', props); - const response = await GatewayApiV1Instance.post('/workspaces/me/keys', { ...props, }); diff --git a/frontend/src/api/IngestionKeys/getAllIngestionKeys.ts b/frontend/src/api/IngestionKeys/getAllIngestionKeys.ts index b1ee6dc933b..a159435fafb 100644 --- a/frontend/src/api/IngestionKeys/getAllIngestionKeys.ts +++ b/frontend/src/api/IngestionKeys/getAllIngestionKeys.ts @@ -1,7 +1,13 @@ import { GatewayApiV1Instance } from 'api'; import { AxiosResponse } from 'axios'; -import { AllIngestionKeyProps } from 'types/api/ingestionKeys/types'; +import { + AllIngestionKeyProps, + GetIngestionKeyProps, +} from 'types/api/ingestionKeys/types'; -export const getAllIngestionKeys = (): Promise< - AxiosResponse -> => GatewayApiV1Instance.get(`/workspaces/me/keys`); +export const getAllIngestionKeys = ( + props: GetIngestionKeyProps, +): Promise> => + GatewayApiV1Instance.get( + `/workspaces/me/keys?page=${props.page}&per_page=${props.per_page}`, + ); diff --git a/frontend/src/api/IngestionKeys/limits/createLimitsForKey.ts b/frontend/src/api/IngestionKeys/limits/createLimitsForKey.ts index d02c190334b..75128b9b780 100644 --- a/frontend/src/api/IngestionKeys/limits/createLimitsForKey.ts +++ b/frontend/src/api/IngestionKeys/limits/createLimitsForKey.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/no-throw-literal */ import { GatewayApiV1Instance } from 'api'; -import axios, { AxiosError } from 'axios'; +import axios from 'axios'; import { - LimitProps, + AddLimitProps, LimitSuccessProps, } from 'types/api/ingestionKeys/limits/types'; @@ -21,11 +21,11 @@ interface ErrorResponse { } const createLimitForIngestionKey = async ( - props: LimitProps, -): Promise | AxiosError> => { + props: AddLimitProps, +): Promise | ErrorResponse> => { try { const response = await GatewayApiV1Instance.post( - `/workspaces/me/keys/${props.keyId}/limits`, + `/workspaces/me/keys/${props.keyID}/limits`, { ...props, }, @@ -39,8 +39,6 @@ const createLimitForIngestionKey = async ( }; } catch (error) { if (axios.isAxiosError(error)) { - console.log('error', error); - // Axios error const errResponse: ErrorResponse = { statusCode: error.response?.status || 500, diff --git a/frontend/src/api/IngestionKeys/limits/updateLimitsForIngestionKey.ts b/frontend/src/api/IngestionKeys/limits/updateLimitsForIngestionKey.ts index bb776e8c83d..89f3031e086 100644 --- a/frontend/src/api/IngestionKeys/limits/updateLimitsForIngestionKey.ts +++ b/frontend/src/api/IngestionKeys/limits/updateLimitsForIngestionKey.ts @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/no-throw-literal */ import { GatewayApiV1Instance } from 'api'; -import axios, { AxiosError } from 'axios'; +import axios from 'axios'; import { - LimitProps, LimitSuccessProps, + UpdateLimitProps, } from 'types/api/ingestionKeys/limits/types'; interface SuccessResponse { @@ -21,13 +21,13 @@ interface ErrorResponse { } const updateLimitForIngestionKey = async ( - props: LimitProps, -): Promise | AxiosError> => { + props: UpdateLimitProps, +): Promise | ErrorResponse> => { try { - const response = await GatewayApiV1Instance.post( - `/workspaces/me/keys/${props.keyId}/limits`, + const response = await GatewayApiV1Instance.patch( + `/workspaces/me/limits/${props.limitID}`, { - ...props, + config: props.config, }, ); @@ -39,8 +39,6 @@ const updateLimitForIngestionKey = async ( }; } catch (error) { if (axios.isAxiosError(error)) { - console.log('error', error); - // Axios error const errResponse: ErrorResponse = { statusCode: error.response?.status || 500, diff --git a/frontend/src/container/IngestionSettings/IngestionKeyDetails.tsx b/frontend/src/container/IngestionSettings/IngestionKeyDetails.tsx deleted file mode 100644 index 0044eb55b94..00000000000 --- a/frontend/src/container/IngestionSettings/IngestionKeyDetails.tsx +++ /dev/null @@ -1,257 +0,0 @@ -/* eslint-disable sonarjs/cognitive-complexity */ -// import './LogDetails.styles.scss'; - -import { Color, Spacing } from '@signozhq/design-tokens'; -import { - Button, - DatePicker, - Divider, - Drawer, - Form, - Input, - Radio, - Select, - Tooltip, - Typography, -} from 'antd'; -import { RadioChangeEvent } from 'antd/lib'; -import cx from 'classnames'; -import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator'; -import ContextView from 'container/LogDetailedView/ContextView/ContextView'; -import JSONView from 'container/LogDetailedView/JsonView'; -import Overview from 'container/LogDetailedView/Overview'; -import { aggregateAttributesResourcesToString } from 'container/LogDetailedView/utils'; -import { useIsDarkMode } from 'hooks/useDarkMode'; -import { useNotifications } from 'hooks/useNotifications'; -import { - BarChart2, - Braces, - Check, - Copy, - DraftingCompass, - Filter, - HardHat, - ScrollText, - Table, - TextSelect, - X, -} from 'lucide-react'; -import { useState } from 'react'; -import { useCopyToClipboard } from 'react-use'; -import { IngestionKeyProps } from 'types/api/ingestionKeys/types'; -import { API_KEY_EXPIRY_OPTIONS, disabledDate } from './IngestionSettings'; -import Tags from 'components/Tags/Tags'; -import { InfoCircleFilled } from '@ant-design/icons'; -import Tabs from 'periscope/components/Tabs'; -import { TabProps } from 'periscope/components/Tabs/Tabs'; -import Limits, { LimitProps } from './Limits/Limits'; -import dayjs from 'dayjs'; - -interface IngestionKeyDetailsProps { - openDrawer: boolean; - onClose: () => void; - data: IngestionKeyProps; - updatedTags: string[]; - onUpdatedTags: (tags: string[]) => void; - handleCopyKey: (key: string) => void; - onUpdateIngestionKeyDetails: (data: Record) => void; -} - -function IngestionKeyDetails({ - openDrawer, - onClose, - data, - updatedTags, - onUpdatedTags, - handleCopyKey, - onUpdateIngestionKeyDetails, -}: IngestionKeyDetailsProps): JSX.Element { - const [, copyToClipboard] = useCopyToClipboard(); - const [editForm] = Form.useForm(); - const isDarkMode = useIsDarkMode(); - - const [signalLimits, setSignalLimits] = useState({ - logs: {}, - metrics: {}, - traces: {}, - }); - - const drawerCloseHandler = ( - e: React.MouseEvent | React.KeyboardEvent, - ): void => { - if (onClose) { - onClose(); - } - }; - - const handleLimitUpdate = ( - id: string, - values: { - [key: string]: LimitProps; - }, - ) => { - setSignalLimits((prevState) => ({ - ...prevState, - [id]: values, - })); - }; - - const handleTabChange = (activeKey: string) => { - console.log('tab change', activeKey); - }; - - const tabItems: TabProps[] = [ - { - label: ( -
- Logs -
- ), - key: 'logs', - children: , - }, - { - label: ( -
- Metrics -
- ), - key: 'metrics', - children: , - }, - { - label: ( -
- Traces -
- ), - key: 'traces', - children: , - }, - ]; - - const handleUpdate = () => { - editForm - .validateFields() - .then((values) => { - onUpdateIngestionKeyDetails({ - ...values, - tags: updatedTags, - limits: signalLimits, - }); - }) - .catch((errorInfo) => { - console.error('error info', errorInfo); - }); - }; - - return ( - - {data?.name} -
- - {data?.value.substring(0, 2)}******** - {data?.value.substring(data.value.length - 2).trim()} - - - { - e.stopPropagation(); - e.preventDefault(); - handleCopyKey(data.value); - }} - /> -
-
- } - placement="right" - open={openDrawer} - onClose={drawerCloseHandler} - style={{ - overscrollBehavior: 'contain', - background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100, - }} - className="ingestion-details-edit-drawer" - destroyOnClose - closeIcon={} - footer={ -
- - - -
- } - > -
- - - - - - - - - - -
-
- -
- -
- You can set limits for both data points and memory used. Data Ingestion - would be limited based on which limit is hit first. -
-
- -
- -
-
- - ); -} - -export default IngestionKeyDetails; diff --git a/frontend/src/container/IngestionSettings/IngestionSettings.tsx b/frontend/src/container/IngestionSettings/IngestionSettings.tsx index 532ec0ca3dd..dba075a5745 100644 --- a/frontend/src/container/IngestionSettings/IngestionSettings.tsx +++ b/frontend/src/container/IngestionSettings/IngestionSettings.tsx @@ -12,7 +12,8 @@ import { Row, Select, Table, - TableProps, + TablePaginationConfig, + TableProps as AntDTableProps, Tag, Typography, } from 'antd'; @@ -20,10 +21,10 @@ 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 deleteLimitsForIngestionKey from 'api/IngestionKeys/limits/deleteLimitsForIngestionKey'; 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 updateLimitForIngestionKey from 'api/IngestionKeys/limits/updateLimitsForIngestionKey'; import { AxiosError } from 'axios'; import Tags from 'components/Tags/Tags'; import { SOMETHING_WENT_WRONG } from 'constants/api'; @@ -49,10 +50,14 @@ import { useMutation } from 'react-query'; import { useSelector } from 'react-redux'; import { useCopyToClipboard } from 'react-use'; import { AppState } from 'store/reducers'; -import { IngestionKeyProps, Limit } from 'types/api/ingestionKeys/types'; +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'; -import { LimitProps, LimitsProps } from './Limits/Limits'; const { Option } = Select; @@ -91,20 +96,44 @@ function IngestionSettings(): 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 [activeSignal, setActiveSignal] = useState(null); const [searchValue, setSearchValue] = 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(); @@ -118,12 +147,9 @@ function IngestionSettings(): JSX.Element { }; const hideDeleteViewModal = (): void => { - console.log('hide delete modal'); setIsDeleteModalOpen(false); setActiveAPIKey(null); handleFormReset(); - - console.log('hide delete modal', activeAPIKey); }; const showDeleteModal = (apiKey: IngestionKeyProps): void => { @@ -132,13 +158,9 @@ function IngestionSettings(): JSX.Element { }; const hideEditViewModal = (): void => { - console.log('hide edit modal'); - setActiveAPIKey(null); setIsEditModalOpen(false); handleFormReset(); - - console.log('hide edit modal', activeAPIKey); }; const hideAddViewModal = (): void => { @@ -148,12 +170,17 @@ function IngestionSettings(): JSX.Element { }; const showEditModal = (apiKey: IngestionKeyProps): void => { - console.log('show edit modal', apiKey); - setActiveAPIKey(apiKey); handleFormReset(); setUpdatedTags(apiKey.tags || []); + + editForm.setFieldsValue({ + name: apiKey.name, + tags: apiKey.tags, + expires_at: dayjs(apiKey?.expires_at) || null, + }); + setIsEditModalOpen(true); }; @@ -164,7 +191,6 @@ function IngestionSettings(): JSX.Element { }; const handleModalClose = (): void => { - console.log('modal close', activeAPIKey); setActiveAPIKey(null); setActiveSignal(null); }; @@ -176,7 +202,7 @@ function IngestionSettings(): JSX.Element { refetch: refetchAPIKeys, error, isError, - } = useGetAllIngestionsKeys(); + } = useGetAllIngestionsKeys(paginationParams); useEffect(() => { setActiveAPIKey(IngestionKeys?.data.data[0]); @@ -184,7 +210,9 @@ function IngestionSettings(): JSX.Element { useEffect(() => { setDataSource(IngestionKeys?.data.data || []); - }, [IngestionKeys?.data.data]); + setTotalIngestionKeys(IngestionKeys?.data?._pagination?.total || 0); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [IngestionKeys?.data?.data]); useEffect(() => { if (isError) { @@ -251,18 +279,38 @@ function IngestionSettings(): JSX.Element { const { mutate: createLimitForIngestionKey, isLoading: isLoadingLimitForKey, - error: createLimitForIngestionKeyError, } = useMutation(createLimitForIngestionKeyApi, { onSuccess: () => { + setActiveSignal(null); + setActiveAPIKey(null); + setIsEditAddLimitOpen(false); setUpdatedTags([]); hideAddViewModal(); refetchAPIKeys(); setHasCreateLimitForIngestionKeyError(false); }, - onError: (error) => { - console.log('on error', error); + onError: (error: ErrorResponse) => { setHasCreateLimitForIngestionKeyError(true); - // showErrorNotification(notifications, error as AxiosError); + 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); }, }); @@ -270,8 +318,9 @@ function IngestionSettings(): JSX.Element { deleteLimitsForIngestionKey, { onSuccess: () => { - refetchAPIKeys(); setIsDeleteModalOpen(false); + setIsDeleteLimitModalOpen(false); + refetchAPIKeys(); }, onError: (error) => { showErrorNotification(notifications, error as AxiosError); @@ -297,6 +346,7 @@ function IngestionSettings(): JSX.Element { data: { name: values.name, tags: updatedTags, + expires_at: dayjs(values.expires_at).endOf('day').toISOString(), }, }); } @@ -339,12 +389,16 @@ function IngestionSettings(): JSX.Element { APIKey: IngestionKeyProps, signalName: string, ): void => { - setActiveSignal(signalName); + setActiveSignal({ + id: signalName, + signal: signalName, + config: {}, + }); const { dailyLimit, secondsLimit } = addEditLimitForm.getFieldsValue(); const payload = { - keyId: APIKey.id, + keyID: APIKey.id, signal: signalName, config: { day: { @@ -359,55 +413,80 @@ function IngestionSettings(): JSX.Element { createLimitForIngestionKey(payload); }; + const handleUpdateLimit = ( + APIKey: IngestionKeyProps, + signal: LimitProps, + ): void => { + setActiveSignal(signal); + const { dailyLimit, secondsLimit } = addEditLimitForm.getFieldsValue(); + const payload = { + limitID: signal.id, + signal: signal.signal, + config: { + day: { + size: parseInt(dailyLimit, 10) * BYTES, + }, + second: { + size: parseInt(secondsLimit, 10) * BYTES, + }, + }, + }; + updateLimitForIngestionKey(payload); + }; + + const getFormattedLimit = (size: number | undefined): number => { + if (!size) { + return 1; + } + + return size / BYTES; + }; + const enableEditLimitMode = ( APIKey: IngestionKeyProps, - signalName: string, + signal: LimitProps, ): void => { setActiveAPIKey(APIKey); - setActiveSignal(signalName); + setActiveSignal(signal); + + addEditLimitForm.setFieldsValue({ + dailyLimit: getFormattedLimit(signal?.config?.day?.size), + secondsLimit: getFormattedLimit(signal?.config?.second?.size), + }); + + setIsEditAddLimitOpen(true); + }; + + const onDeleteLimitHandler = (): void => { + if (activeSignal && activeSignal?.id) { + deleteLimitForKey(activeSignal.id); + } }; - const handleDeleteLimit = ( + const showDeleteLimitModal = ( APIKey: IngestionKeyProps, - signal: LimitsProps, + limit: LimitProps, ): void => { - console.log('delete - api key', APIKey); - console.log('signalName key', signal); + setActiveAPIKey(APIKey); + setActiveSignal(limit); + setIsDeleteLimitModalOpen(true); + }; - if (signal) { - deleteLimitForKey(signal.id); - } + const hideDeleteLimitModal = (): void => { + setIsDeleteLimitModalOpen(false); }; const handleDiscardSaveLimit = (): void => { setHasCreateLimitForIngestionKeyError(false); + setHasUpdateLimitForIngestionKeyError(false); + setIsEditAddLimitOpen(false); setActiveAPIKey(null); setActiveSignal(null); addEditLimitForm.resetFields(); }; - const handleSaveLimit = ( - APIKey: IngestionKeyProps, - signal: string, - addOrUpdate: string, - ): void => { - if (addOrUpdate === 'add') { - handleAddLimit(APIKey, signal); - } else { - // handleAddLimit(APIKey, signal); - } - }; - - const getFormattedLimit = (size: number | undefined): number => { - if (!size) { - return 1; - } - - return size / BYTES; - }; - - const columns: TableProps['columns'] = [ + const columns: AntDTableProps['columns'] = [ { title: 'Ingestion Key', key: 'ingestion-key', @@ -419,16 +498,14 @@ function IngestionSettings(): JSX.Element { const updatedOn = getFormattedTime(APIKey?.updated_at); - const limits: { [key: string]: Limit } = {}; + const limits: { [key: string]: LimitProps } = {}; - APIKey.limits?.forEach((limit: Limit) => { + APIKey.limits?.forEach((limit: LimitProps) => { limits[limit.signal] = limit; }); const hasLimits = (signal: string): boolean => !!limits[signal]; - console.log('limits', limits); - const items: CollapseProps['items'] = [ { key: '1', @@ -533,7 +610,7 @@ function IngestionSettings(): JSX.Element { onClick={(e): void => { e.stopPropagation(); e.preventDefault(); - enableEditLimitMode(APIKey, signal); + enableEditLimitMode(APIKey, limits[signal]); }} /> @@ -544,8 +621,7 @@ function IngestionSettings(): JSX.Element { onClick={(e): void => { e.stopPropagation(); e.preventDefault(); - - handleDeleteLimit(APIKey, limits[signal]); + showDeleteLimitModal(APIKey, limits[signal]); }} /> @@ -560,7 +636,12 @@ function IngestionSettings(): JSX.Element { onClick={(e): void => { e.stopPropagation(); e.preventDefault(); - enableEditLimitMode(APIKey, signal); + + enableEditLimitMode(APIKey, { + id: signal, + signal, + config: {}, + }); }} > Limits @@ -570,7 +651,9 @@ function IngestionSettings(): JSX.Element {
- {activeAPIKey?.id === APIKey.id && activeSignal === signal && ( + {activeAPIKey?.id === APIKey.id && + activeSignal?.signal === signal && + isEditAddLimitOpen ? (
{activeAPIKey?.id === APIKey.id && - activeSignal === signal && + activeSignal.signal === signal && !isLoadingLimitForKey && hasCreateLimitForIngestionKeyError && createLimitForIngestionKeyError && @@ -652,39 +735,55 @@ function IngestionSettings(): JSX.Element {
)} - {activeAPIKey?.id === APIKey.id && activeSignal === signal && ( -
- - -
- )} - - )} + {activeAPIKey?.id === APIKey.id && + activeSignal.signal === signal && + !isLoadingLimitForKey && + hasUpdateLimitForIngestionKeyError && + updateLimitForIngestionKeyError && ( +
+ {updateLimitForIngestionKeyError?.error} +
+ )} - {(activeAPIKey?.id !== APIKey.id || - !activeSignal || - activeSignal !== signal) && ( + {activeAPIKey?.id === APIKey.id && + activeSignal.signal === signal && + isEditAddLimitOpen && ( +
+ + +
+ )} + + ) : (
@@ -751,6 +850,13 @@ function IngestionSettings(): JSX.Element { }, ]; + const handleTableChange = (pagination: TablePaginationConfig): void => { + setPaginationParams({ + page: pagination?.current || 1, + per_page: 10, + }); + }; + return (
@@ -783,11 +889,13 @@ function IngestionSettings(): JSX.Element { dataSource={dataSource} loading={isLoading || isRefetching} showHeader={false} + onChange={handleTableChange} pagination={{ - pageSize: 5, + pageSize: paginationParams?.per_page, hideOnSinglePage: true, showTotal: (total: number, range: number[]): string => `${range[0]}-${range[1]} of ${total} Ingestion keys`, + total: totalIngestionKeys, }} />
@@ -828,12 +936,49 @@ function IngestionSettings(): JSX.Element { + {/* 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 */} void; -} - -export default function Limits({ id, onLimitUpdate }: LimitsProps) { - const [limitValues, setLimitValues] = useState<{ - [key: string]: LimitProps; - }>(() => { - const initialLimits: { - [key: string]: LimitProps; - } = {}; - - LIMITS.forEach((limit) => { - initialLimits[limit.id] = { - size: 0, - count: 0, - sizeUnit: 'GB', - enabled: false, - }; - }); - return initialLimits; - }); - - const handleChange = ( - limitId: string, - field: string, - value: number | string | boolean, - ) => { - setLimitValues((prevState) => ({ - ...prevState, - [limitId]: { - ...prevState[limitId], - [field]: value, - }, - })); - }; - - const handleBlur = ( - limitId: string, - field: string, - value: number | string | boolean, - ) => { - // Call handleChange only on blur - handleChange(limitId, field, value); - }; - - useEffect(() => { - onLimitUpdate(id, limitValues); - }, [limitValues]); - - return ( -
- {LIMITS.map((limit) => { - return ( -
-
-
-
{limit.title}
- -
{limit.subTitle}
-
- -
- } - unCheckedChildren={} - defaultChecked={limitValues[limit.id].enabled} - onChange={(value) => handleBlur(limit.id, 'enabled', value)} - /> -
-
- -
-
- handleBlur(limit.id, 'sizeUnit', value)} - disabled={!limitValues[limit.id].enabled} - > - - - - - - } - onBlur={(e) => - handleBlur( - limit.id, - 'size', - e.target.value ? parseInt(e.target.value) : 0, - ) - } - /> -
- -
OR
-
- - handleBlur( - limit.id, - 'count', - e.target.value ? parseInt(e.target.value) : 0, - ) - } - /> -
-
-
- ); - })} -
- ); -} diff --git a/frontend/src/hooks/IngestionKeys/useGetAllIngestionKeys.ts b/frontend/src/hooks/IngestionKeys/useGetAllIngestionKeys.ts index 400c96d5d45..4620f048be7 100644 --- a/frontend/src/hooks/IngestionKeys/useGetAllIngestionKeys.ts +++ b/frontend/src/hooks/IngestionKeys/useGetAllIngestionKeys.ts @@ -1,13 +1,15 @@ import { getAllIngestionKeys } from 'api/IngestionKeys/getAllIngestionKeys'; import { AxiosError, AxiosResponse } from 'axios'; import { useQuery, UseQueryResult } from 'react-query'; -import { AllIngestionKeyProps } from 'types/api/ingestionKeys/types'; +import { + AllIngestionKeyProps, + GetIngestionKeyProps, +} from 'types/api/ingestionKeys/types'; -export const useGetAllIngestionsKeys = (): UseQueryResult< - AxiosResponse, - AxiosError -> => +export const useGetAllIngestionsKeys = ( + props: GetIngestionKeyProps, +): UseQueryResult, AxiosError> => useQuery, AxiosError>({ - queryKey: ['IngestionKeys'], - queryFn: () => getAllIngestionKeys(), + queryKey: [`IngestionKeys-${props.page}`], + queryFn: () => getAllIngestionKeys(props), }); diff --git a/frontend/src/types/api/ingestionKeys/limits/types.ts b/frontend/src/types/api/ingestionKeys/limits/types.ts index 14fc9cc1ede..665352a7575 100644 --- a/frontend/src/types/api/ingestionKeys/limits/types.ts +++ b/frontend/src/types/api/ingestionKeys/limits/types.ts @@ -1,5 +1,35 @@ export interface LimitProps { - keyId: string; + id: string; + signal: string; + tags?: string[]; + key_id?: string; + created_at?: string; + updated_at?: string; + config?: { + day?: { + size?: number; + }; + second?: { + size?: number; + }; + }; +} + +export interface AddLimitProps { + keyID: string; + signal: string; + config: { + day: { + size: number; + }; + second: { + size: number; + }; + }; +} + +export interface UpdateLimitProps { + limitID: string; signal: string; config: { day: { diff --git a/frontend/src/types/api/ingestionKeys/types.ts b/frontend/src/types/api/ingestionKeys/types.ts index b51c4453223..3941f95600a 100644 --- a/frontend/src/types/api/ingestionKeys/types.ts +++ b/frontend/src/types/api/ingestionKeys/types.ts @@ -9,6 +9,7 @@ export interface User { export interface Limit { signal: string; + id: string; config?: { day?: { size?: number; @@ -32,15 +33,28 @@ export interface IngestionKeyProps { limits?: Limit[]; } +export interface GetIngestionKeyProps { + page: number; + per_page: number; +} + export interface CreateIngestionKeyProps { name: string; expires_at: string; tags: string[]; } +export interface PaginationProps { + page: number; + per_page: number; + pages?: number; + total?: number; +} + export interface AllIngestionKeyProps { status: string; data: IngestionKeyProps[]; + _pagination: PaginationProps; } export interface CreateIngestionKeyProp { @@ -55,7 +69,7 @@ export interface UpdateIngestionKeyProps { id: string; data: { name: string; - expires_at?: number; + expires_at: string; tags: string[]; }; } From 46ca9fb58efb5e169e10cbceefefd75c0ca42e03 Mon Sep 17 00:00:00 2001 From: Yunus A M Date: Mon, 3 Jun 2024 16:41:44 +0530 Subject: [PATCH 08/14] fix: lint errors --- frontend/src/periscope/components/Tabs/Tabs.tsx | 4 +++- frontend/src/periscope/components/Tabs/index.tsx | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/periscope/components/Tabs/Tabs.tsx b/frontend/src/periscope/components/Tabs/Tabs.tsx index 96eb2e50ad5..219a09e3f28 100644 --- a/frontend/src/periscope/components/Tabs/Tabs.tsx +++ b/frontend/src/periscope/components/Tabs/Tabs.tsx @@ -1,4 +1,6 @@ +/* eslint-disable react/jsx-props-no-spreading */ import { Tabs as AntDTabs, TabsProps } from 'antd'; +import React from 'react'; export interface TabProps { label: string | React.ReactElement; @@ -6,6 +8,6 @@ export interface TabProps { children: React.ReactElement; } -export default function Tabs(props: TabsProps) { +export default function Tabs(props: TabsProps): React.ReactNode { return ; } diff --git a/frontend/src/periscope/components/Tabs/index.tsx b/frontend/src/periscope/components/Tabs/index.tsx index e0629d98456..4b674c987b6 100644 --- a/frontend/src/periscope/components/Tabs/index.tsx +++ b/frontend/src/periscope/components/Tabs/index.tsx @@ -1,3 +1,3 @@ import Tabs from './Tabs'; -export default Tabs; \ No newline at end of file +export default Tabs; From 290c0ee16acc87b3c60426383f7712ca4ed1e9d0 Mon Sep 17 00:00:00 2001 From: Yunus A M Date: Mon, 3 Jun 2024 17:21:36 +0530 Subject: [PATCH 09/14] feat: support query search for ingestion name --- .../api/IngestionKeys/getAllIngestionKeys.ts | 16 ++++++++++++---- .../IngestionSettings/IngestionSettings.tsx | 19 +++++++++++-------- .../IngestionKeys/useGetAllIngestionKeys.ts | 2 +- frontend/src/types/api/ingestionKeys/types.ts | 1 + 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/frontend/src/api/IngestionKeys/getAllIngestionKeys.ts b/frontend/src/api/IngestionKeys/getAllIngestionKeys.ts index a159435fafb..e202917445d 100644 --- a/frontend/src/api/IngestionKeys/getAllIngestionKeys.ts +++ b/frontend/src/api/IngestionKeys/getAllIngestionKeys.ts @@ -7,7 +7,15 @@ import { export const getAllIngestionKeys = ( props: GetIngestionKeyProps, -): Promise> => - GatewayApiV1Instance.get( - `/workspaces/me/keys?page=${props.page}&per_page=${props.per_page}`, - ); +): Promise> => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { search, per_page, page } = props; + + const BASE_URL = '/workspaces/me/keys'; + const URL_QUERY_PARAMS = + search && search.length > 0 + ? `/search?name=${search}&page=1&per_page=100` + : `?page=${page}&per_page=${per_page}`; + + return GatewayApiV1Instance.get(`${BASE_URL}${URL_QUERY_PARAMS}`); +}; diff --git a/frontend/src/container/IngestionSettings/IngestionSettings.tsx b/frontend/src/container/IngestionSettings/IngestionSettings.tsx index dba075a5745..eadd93f9025 100644 --- a/frontend/src/container/IngestionSettings/IngestionSettings.tsx +++ b/frontend/src/container/IngestionSettings/IngestionSettings.tsx @@ -30,6 +30,7 @@ import Tags from 'components/Tags/Tags'; import { SOMETHING_WENT_WRONG } from 'constants/api'; import dayjs, { Dayjs } from 'dayjs'; import { useGetAllIngestionsKeys } from 'hooks/IngestionKeys/useGetAllIngestionKeys'; +import useDebouncedFn from 'hooks/useDebouncedFunction'; import { useNotifications } from 'hooks/useNotifications'; import { CalendarClock, @@ -106,6 +107,7 @@ function IngestionSettings(): JSX.Element { const [activeSignal, setActiveSignal] = useState(null); const [searchValue, setSearchValue] = useState(''); + const [searchText, setSearchText] = useState(''); const [dataSource, setDataSource] = useState([]); const [paginationParams, setPaginationParams] = useState({ page: 1, @@ -202,7 +204,10 @@ function IngestionSettings(): JSX.Element { refetch: refetchAPIKeys, error, isError, - } = useGetAllIngestionsKeys(paginationParams); + } = useGetAllIngestionsKeys({ + search: searchText, + ...paginationParams, + }); useEffect(() => { setActiveAPIKey(IngestionKeys?.data.data[0]); @@ -220,15 +225,13 @@ function IngestionSettings(): JSX.Element { } }, [error, isError, notifications]); + const handleDebouncedSearch = useDebouncedFn((searchText): void => { + setSearchText(searchText as string); + }, 500); + const handleSearch = (e: ChangeEvent): void => { setSearchValue(e.target.value); - const filteredData = IngestionKeys?.data?.data?.filter( - (key: IngestionKeyProps) => - key && - key.name && - key.name.toLowerCase().includes(e.target.value.toLowerCase()), - ); - setDataSource(filteredData || []); + handleDebouncedSearch(e.target.value || ''); }; const clearSearch = (): void => { diff --git a/frontend/src/hooks/IngestionKeys/useGetAllIngestionKeys.ts b/frontend/src/hooks/IngestionKeys/useGetAllIngestionKeys.ts index 4620f048be7..8c4a19a0e9a 100644 --- a/frontend/src/hooks/IngestionKeys/useGetAllIngestionKeys.ts +++ b/frontend/src/hooks/IngestionKeys/useGetAllIngestionKeys.ts @@ -10,6 +10,6 @@ export const useGetAllIngestionsKeys = ( props: GetIngestionKeyProps, ): UseQueryResult, AxiosError> => useQuery, AxiosError>({ - queryKey: [`IngestionKeys-${props.page}`], + queryKey: [`IngestionKeys-${props.page}-${props.search}`], queryFn: () => getAllIngestionKeys(props), }); diff --git a/frontend/src/types/api/ingestionKeys/types.ts b/frontend/src/types/api/ingestionKeys/types.ts index 3941f95600a..b29c539cb16 100644 --- a/frontend/src/types/api/ingestionKeys/types.ts +++ b/frontend/src/types/api/ingestionKeys/types.ts @@ -36,6 +36,7 @@ export interface IngestionKeyProps { export interface GetIngestionKeyProps { page: number; per_page: number; + search?: string; } export interface CreateIngestionKeyProps { From 5b547872db104bc7ec8234e370de196e4b6fec7a Mon Sep 17 00:00:00 2001 From: Yunus A M Date: Tue, 4 Jun 2024 00:31:40 +0530 Subject: [PATCH 10/14] feat: show utilized size in limits --- .../container/IngestionSettings/IngestionSettings.tsx | 9 ++++++--- frontend/src/types/api/ingestionKeys/limits/types.ts | 8 ++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/frontend/src/container/IngestionSettings/IngestionSettings.tsx b/frontend/src/container/IngestionSettings/IngestionSettings.tsx index eadd93f9025..bd7705e8c29 100644 --- a/frontend/src/container/IngestionSettings/IngestionSettings.tsx +++ b/frontend/src/container/IngestionSettings/IngestionSettings.tsx @@ -439,7 +439,7 @@ function IngestionSettings(): JSX.Element { const getFormattedLimit = (size: number | undefined): number => { if (!size) { - return 1; + return 0; } return size / BYTES; @@ -796,6 +796,7 @@ function IngestionSettings(): JSX.Element {
{limits[signal]?.config?.day?.size ? ( <> + {getFormattedLimit(limits[signal]?.metric?.day?.size)} GB /{' '} {getFormattedLimit(limits[signal]?.config?.day?.size)} GB ) : ( @@ -812,9 +813,11 @@ function IngestionSettings(): JSX.Element {
- {limits[signal]?.config?.day?.size ? ( + {limits[signal]?.config?.second?.size ? ( <> - {getFormattedLimit(limits[signal]?.config?.second?.size)} GB + {getFormattedLimit(limits[signal]?.metric?.second?.size)} GB + / {getFormattedLimit(limits[signal]?.config?.second?.size)}{' '} + GB ) : ( <> diff --git a/frontend/src/types/api/ingestionKeys/limits/types.ts b/frontend/src/types/api/ingestionKeys/limits/types.ts index 665352a7575..279ec157d5a 100644 --- a/frontend/src/types/api/ingestionKeys/limits/types.ts +++ b/frontend/src/types/api/ingestionKeys/limits/types.ts @@ -13,6 +13,14 @@ export interface LimitProps { size?: number; }; }; + metric?: { + day?: { + size?: number; + }; + second?: { + size?: number; + }; + }; } export interface AddLimitProps { From 59b230d11639bf7db0979776168c055a5568afe4 Mon Sep 17 00:00:00 2001 From: Yunus A M Date: Tue, 4 Jun 2024 13:03:49 +0530 Subject: [PATCH 11/14] feat: show multiple ingestions ui only if gateway is enabled --- frontend/src/constants/features.ts | 1 + .../IngestionSettings/IngestionSettings.tsx | 1168 +---------------- .../MultiIngestionSettings.tsx | 1133 ++++++++++++++++ frontend/src/pages/Settings/config.tsx | 16 + frontend/src/pages/Settings/index.tsx | 13 +- frontend/src/pages/Settings/utils.ts | 9 +- 6 files changed, 1229 insertions(+), 1111 deletions(-) create mode 100644 frontend/src/container/IngestionSettings/MultiIngestionSettings.tsx diff --git a/frontend/src/constants/features.ts b/frontend/src/constants/features.ts index 67f9fe6110f..bb905d0d694 100644 --- a/frontend/src/constants/features.ts +++ b/frontend/src/constants/features.ts @@ -20,4 +20,5 @@ export enum FeatureKeys { ONBOARDING = 'ONBOARDING', CHAT_SUPPORT = 'CHAT_SUPPORT', PLANNED_MAINTENANCE = 'PLANNED_MAINTENANCE', + GATEWAY = 'GATEWAY', } diff --git a/frontend/src/container/IngestionSettings/IngestionSettings.tsx b/frontend/src/container/IngestionSettings/IngestionSettings.tsx index bd7705e8c29..c84543ca4e5 100644 --- a/frontend/src/container/IngestionSettings/IngestionSettings.tsx +++ b/frontend/src/container/IngestionSettings/IngestionSettings.tsx @@ -1,1133 +1,91 @@ import './IngestionSettings.styles.scss'; -import { Color } from '@signozhq/design-tokens'; -import { - Button, - Col, - Collapse, - DatePicker, - Form, - Input, - Modal, - Row, - Select, - 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 Tags from 'components/Tags/Tags'; -import { SOMETHING_WENT_WRONG } from 'constants/api'; -import dayjs, { Dayjs } from 'dayjs'; -import { useGetAllIngestionsKeys } from 'hooks/IngestionKeys/useGetAllIngestionKeys'; -import useDebouncedFn from 'hooks/useDebouncedFunction'; -import { useNotifications } from 'hooks/useNotifications'; -import { - CalendarClock, - Check, - Copy, - Infinity, - Minus, - PenLine, - Plus, - PlusIcon, - Search, - Trash2, - X, -} from 'lucide-react'; -import { ChangeEvent, useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useMutation } from 'react-query'; +import { Skeleton, Table, Typography } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import getIngestionData from 'api/settings/getIngestionData'; +import { useQuery } 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 { IngestionDataType } from 'types/api/settings/ingestion'; import AppReducer from 'types/reducer/app'; -import { USER_ROLES } from 'types/roles'; -const { Option } = Select; - -const BYTES = 1073741824; - -export const disabledDate = (current: Dayjs): 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 IngestionSettings(): JSX.Element { +export default function IngestionSettings(): 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, + const { data: ingestionData, isFetching } = useQuery({ + queryFn: getIngestionData, + queryKey: ['getIngestionData', user?.userId], }); - 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, + const columns: ColumnsType = [ { - onSuccess: () => { - refetchAPIKeys(); - setIsEditModalOpen(false); - }, - onError: (error) => { - showErrorNotification(notifications, error as AxiosError); - }, + title: 'Name', + dataIndex: 'name', + key: 'name', + render: (text): JSX.Element => {text} , }, - ); - - const { mutate: deleteAPIKey, isLoading: isDeleteingAPIKey } = useMutation( - deleteIngestionKey, { - onSuccess: () => { - refetchAPIKeys(); - setIsDeleteModalOpen(false); - }, - onError: (error) => { - showErrorNotification(notifications, error as AxiosError); - }, + title: '', + dataIndex: 'value', + key: 'value', + render: (text): JSX.Element => ( +
+ {isFetching ? ( + + ) : ( + + {text} + + )} +
+ ), }, - ); + ]; - 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 injectionDataPayload = + ingestionData && + ingestionData.payload && + Array.isArray(ingestionData.payload) && + ingestionData?.payload[0]; - 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 data: IngestionDataType[] = [ + { + key: '1', + name: 'Ingestion URL', + value: injectionDataPayload?.ingestionURL, }, - }); - - const { mutate: deleteLimitForKey, isLoading: isDeletingLimit } = useMutation( - deleteLimitsForIngestionKey, { - onSuccess: () => { - setIsDeleteModalOpen(false); - setIsDeleteLimitModalOpen(false); - refetchAPIKeys(); - }, - onError: (error) => { - showErrorNotification(notifications, error as AxiosError); - }, + key: '2', + name: 'Ingestion Key', + value: injectionDataPayload?.ingestionKey, }, - ); - - 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 getFormattedTime = (date: string): string => - dayjs(date).format('MMM DD,YYYY, hh:mm a'); - - const handleAddLimit = ( - APIKey: IngestionKeyProps, - signalName: string, - ): void => { - setActiveSignal({ - id: signalName, - signal: signalName, - config: {}, - }); - - const { dailyLimit, secondsLimit } = addEditLimitForm.getFieldsValue(); - - const payload = { - keyID: APIKey.id, - signal: signalName, - config: { - day: { - size: parseInt(dailyLimit, 10) * BYTES, - }, - second: { - size: parseInt(secondsLimit, 10) * BYTES, - }, - }, - }; - - createLimitForIngestionKey(payload); - }; - - const handleUpdateLimit = ( - APIKey: IngestionKeyProps, - signal: LimitProps, - ): void => { - setActiveSignal(signal); - const { dailyLimit, secondsLimit } = addEditLimitForm.getFieldsValue(); - const payload = { - limitID: signal.id, - signal: signal.signal, - config: { - day: { - size: parseInt(dailyLimit, 10) * BYTES, - }, - second: { - size: parseInt(secondsLimit, 10) * BYTES, - }, - }, - }; - updateLimitForIngestionKey(payload); - }; - - const getFormattedLimit = (size: number | undefined): number => { - if (!size) { - return 0; - } - - return size / BYTES; - }; - - const enableEditLimitMode = ( - APIKey: IngestionKeyProps, - signal: LimitProps, - ): void => { - setActiveAPIKey(APIKey); - setActiveSignal(signal); - - addEditLimitForm.setFieldsValue({ - dailyLimit: getFormattedLimit(signal?.config?.day?.size), - secondsLimit: getFormattedLimit(signal?.config?.second?.size), - }); - - setIsEditAddLimitOpen(true); - }; - - const onDeleteLimitHandler = (): void => { - if (activeSignal && activeSignal?.id) { - deleteLimitForKey(activeSignal.id); - } - }; - - 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 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); - const formattedDateAndTime = - APIKey && APIKey?.expires_at && getFormattedTime(APIKey?.expires_at); - - const updatedOn = getFormattedTime(APIKey?.updated_at); - - 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) => ( -
-
-
{signal}
-
- {hasLimits(signal) ? ( - <> - - )} -
-
- -
- {activeAPIKey?.id === APIKey.id && - activeSignal?.signal === signal && - isEditAddLimitOpen ? ( -
-
-
-
-
Daily limit
-
- Add a limit for data ingested daily{' '} -
-
- -
- - - - - - - - } - /> - -
-
- -
-
-
Per Second limit
-
- {' '} - Add a limit for data ingested every second{' '} -
-
- -
- - - - - - - - } - /> - -
-
-
- - {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 {' '} -
- -
- {limits[signal]?.config?.day?.size ? ( - <> - {getFormattedLimit(limits[signal]?.metric?.day?.size)} GB /{' '} - {getFormattedLimit(limits[signal]?.config?.day?.size)} GB - - ) : ( - <> - NO LIMIT - - )} -
-
- -
-
- Seconds -
- -
- {limits[signal]?.config?.second?.size ? ( - <> - {getFormattedLimit(limits[signal]?.metric?.second?.size)} GB - / {getFormattedLimit(limits[signal]?.config?.second?.size)}{' '} - GB - - ) : ( - <> - NO LIMIT - - )} -
-
-
- )} -
-
- ))} -
-
-
- - ), - }, - ]; - - return ( -
- - -
-
- - Expires on - {formattedDateAndTime} -
-
-
- ); - }, + key: '3', + name: 'Ingestion Region', + value: injectionDataPayload?.dataRegion, }, ]; - const handleTableChange = (pagination: TablePaginationConfig): void => { - setPaginationParams({ - page: pagination?.current || 1, - per_page: 10, - }); - }; - return ( -
-
-
- Ingestion Keys - - Create and manage ingestion keys for the SigNoz Cloud - -
- -
- } - 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 - , - , - ]} - > -
- - - - - - - - - - - - -
+ You can use the following ingestion credentials to start sending your + telemetry data to SigNoz +
+ +
); } - -export default IngestionSettings; diff --git a/frontend/src/container/IngestionSettings/MultiIngestionSettings.tsx b/frontend/src/container/IngestionSettings/MultiIngestionSettings.tsx new file mode 100644 index 00000000000..e7bcd8830af --- /dev/null +++ b/frontend/src/container/IngestionSettings/MultiIngestionSettings.tsx @@ -0,0 +1,1133 @@ +import './IngestionSettings.styles.scss'; + +import { Color } from '@signozhq/design-tokens'; +import { + Button, + Col, + Collapse, + DatePicker, + Form, + Input, + Modal, + Row, + Select, + 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 Tags from 'components/Tags/Tags'; +import { SOMETHING_WENT_WRONG } from 'constants/api'; +import dayjs, { Dayjs } from 'dayjs'; +import { useGetAllIngestionsKeys } from 'hooks/IngestionKeys/useGetAllIngestionKeys'; +import useDebouncedFn from 'hooks/useDebouncedFunction'; +import { useNotifications } from 'hooks/useNotifications'; +import { + CalendarClock, + Check, + Copy, + Infinity, + Minus, + PenLine, + Plus, + PlusIcon, + Search, + Trash2, + X, +} from 'lucide-react'; +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; + +export const disabledDate = (current: Dayjs): 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 getFormattedTime = (date: string): string => + dayjs(date).format('MMM DD,YYYY, hh:mm a'); + + const handleAddLimit = ( + APIKey: IngestionKeyProps, + signalName: string, + ): void => { + setActiveSignal({ + id: signalName, + signal: signalName, + config: {}, + }); + + const { dailyLimit, secondsLimit } = addEditLimitForm.getFieldsValue(); + + const payload = { + keyID: APIKey.id, + signal: signalName, + config: { + day: { + size: parseInt(dailyLimit, 10) * BYTES, + }, + second: { + size: parseInt(secondsLimit, 10) * BYTES, + }, + }, + }; + + createLimitForIngestionKey(payload); + }; + + const handleUpdateLimit = ( + APIKey: IngestionKeyProps, + signal: LimitProps, + ): void => { + setActiveSignal(signal); + const { dailyLimit, secondsLimit } = addEditLimitForm.getFieldsValue(); + const payload = { + limitID: signal.id, + signal: signal.signal, + config: { + day: { + size: parseInt(dailyLimit, 10) * BYTES, + }, + second: { + size: parseInt(secondsLimit, 10) * BYTES, + }, + }, + }; + updateLimitForIngestionKey(payload); + }; + + const getFormattedLimit = (size: number | undefined): number => { + if (!size) { + return 0; + } + + return size / BYTES; + }; + + const enableEditLimitMode = ( + APIKey: IngestionKeyProps, + signal: LimitProps, + ): void => { + setActiveAPIKey(APIKey); + setActiveSignal(signal); + + addEditLimitForm.setFieldsValue({ + dailyLimit: getFormattedLimit(signal?.config?.day?.size), + secondsLimit: getFormattedLimit(signal?.config?.second?.size), + }); + + setIsEditAddLimitOpen(true); + }; + + const onDeleteLimitHandler = (): void => { + if (activeSignal && activeSignal?.id) { + deleteLimitForKey(activeSignal.id); + } + }; + + 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 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); + const formattedDateAndTime = + APIKey && APIKey?.expires_at && getFormattedTime(APIKey?.expires_at); + + const updatedOn = getFormattedTime(APIKey?.updated_at); + + 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) => ( +
+
+
{signal}
+
+ {hasLimits(signal) ? ( + <> + + )} +
+
+ +
+ {activeAPIKey?.id === APIKey.id && + activeSignal?.signal === signal && + isEditAddLimitOpen ? ( +
+
+
+
+
Daily limit
+
+ Add a limit for data ingested daily{' '} +
+
+ +
+ + + + + + + + } + /> + +
+
+ +
+
+
Per Second limit
+
+ {' '} + Add a limit for data ingested every second{' '} +
+
+ +
+ + + + + + + + } + /> + +
+
+
+ + {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 {' '} +
+ +
+ {limits[signal]?.config?.day?.size ? ( + <> + {getFormattedLimit(limits[signal]?.metric?.day?.size)} GB /{' '} + {getFormattedLimit(limits[signal]?.config?.day?.size)} GB + + ) : ( + <> + NO LIMIT + + )} +
+
+ +
+
+ Seconds +
+ +
+ {limits[signal]?.config?.second?.size ? ( + <> + {getFormattedLimit(limits[signal]?.metric?.second?.size)} GB + / {getFormattedLimit(limits[signal]?.config?.second?.size)}{' '} + GB + + ) : ( + <> + NO LIMIT + + )} +
+
+
+ )} +
+
+ ))} +
+
+
+ + ), + }, + ]; + + return ( +
+ + +
+
+ + Expires on + {formattedDateAndTime} +
+
+
+ ); + }, + }, + ]; + + const handleTableChange = (pagination: TablePaginationConfig): void => { + setPaginationParams({ + page: pagination?.current || 1, + per_page: 10, + }); + }; + + return ( +
+
+
+ Ingestion Keys + + Create and manage ingestion keys for the SigNoz Cloud + +
+ +
+ } + 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; diff --git a/frontend/src/pages/Settings/config.tsx b/frontend/src/pages/Settings/config.tsx index c6073ce3220..94b06a55471 100644 --- a/frontend/src/pages/Settings/config.tsx +++ b/frontend/src/pages/Settings/config.tsx @@ -5,6 +5,7 @@ import APIKeys from 'container/APIKeys/APIKeys'; import GeneralSettings from 'container/GeneralSettings'; import GeneralSettingsCloud from 'container/GeneralSettingsCloud'; import IngestionSettings from 'container/IngestionSettings/IngestionSettings'; +import MultiIngestionSettings from 'container/IngestionSettings/MultiIngestionSettings'; import OrganizationSettings from 'container/OrganizationSettings'; import { TFunction } from 'i18next'; import { Backpack, BellDot, Building, Cpu, KeySquare } from 'lucide-react'; @@ -48,6 +49,21 @@ export const ingestionSettings = (t: TFunction): RouteTabProps['routes'] => [ }, ]; +export const multiIngestionSettings = ( + t: TFunction, +): RouteTabProps['routes'] => [ + { + Component: MultiIngestionSettings, + name: ( +
+ {t('routes:ingestion_settings').toString()} +
+ ), + route: ROUTES.INGESTION_SETTINGS, + key: ROUTES.INGESTION_SETTINGS, + }, +]; + export const generalSettings = (t: TFunction): RouteTabProps['routes'] => [ { Component: GeneralSettings, diff --git a/frontend/src/pages/Settings/index.tsx b/frontend/src/pages/Settings/index.tsx index c2bdcef5e61..bef3d7cba80 100644 --- a/frontend/src/pages/Settings/index.tsx +++ b/frontend/src/pages/Settings/index.tsx @@ -1,5 +1,7 @@ import RouteTab from 'components/RouteTab'; +import { FeatureKeys } from 'constants/features'; import useComponentPermission from 'hooks/useComponentPermission'; +import useFeatureFlag from 'hooks/useFeatureFlag'; import history from 'lib/history'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -19,11 +21,12 @@ function SettingsPage(): JSX.Element { ); const { t } = useTranslation(['routes']); - const routes = useMemo(() => getRoutes(role, isCurrentOrgSettings, t), [ - role, - isCurrentOrgSettings, - t, - ]); + const isGatewayEnabled = !!useFeatureFlag(FeatureKeys.GATEWAY)?.active; + + const routes = useMemo( + () => getRoutes(role, isCurrentOrgSettings, isGatewayEnabled, t), + [role, isCurrentOrgSettings, isGatewayEnabled, t], + ); return ; } diff --git a/frontend/src/pages/Settings/utils.ts b/frontend/src/pages/Settings/utils.ts index 6078ef76210..cb2a5869676 100644 --- a/frontend/src/pages/Settings/utils.ts +++ b/frontend/src/pages/Settings/utils.ts @@ -8,12 +8,14 @@ import { apiKeys, generalSettings, ingestionSettings, + multiIngestionSettings, organizationSettings, } from './config'; export const getRoutes = ( userRole: ROLES | null, isCurrentOrgSettings: boolean, + isGatewayEnabled: boolean, t: TFunction, ): RouteTabProps['routes'] => { const settings = []; @@ -25,7 +27,12 @@ export const getRoutes = ( } if (isCloudUser()) { - settings.push(...ingestionSettings(t)); + if (!isGatewayEnabled) { + settings.push(...multiIngestionSettings(t)); + } else { + settings.push(...ingestionSettings(t)); + } + settings.push(...alertChannels(t)); } else { settings.push(...alertChannels(t)); From 8f9a504d65fdfadb7b3a01e917e9558c3e6e52cc Mon Sep 17 00:00:00 2001 From: Yunus A M Date: Tue, 4 Jun 2024 17:20:33 +0530 Subject: [PATCH 12/14] feat: handle decimal values for ingestion size --- .../MultiIngestionSettings.tsx | 77 +++++++++++-------- frontend/src/pages/Settings/utils.ts | 2 +- 2 files changed, 44 insertions(+), 35 deletions(-) diff --git a/frontend/src/container/IngestionSettings/MultiIngestionSettings.tsx b/frontend/src/container/IngestionSettings/MultiIngestionSettings.tsx index e7bcd8830af..7d704f04328 100644 --- a/frontend/src/container/IngestionSettings/MultiIngestionSettings.tsx +++ b/frontend/src/container/IngestionSettings/MultiIngestionSettings.tsx @@ -8,6 +8,7 @@ import { DatePicker, Form, Input, + InputNumber, Modal, Row, Select, @@ -26,6 +27,7 @@ import deleteLimitsForIngestionKey from 'api/IngestionKeys/limits/deleteLimitsFo 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, { Dayjs } from 'dayjs'; @@ -385,6 +387,8 @@ function MultiIngestionSettings(): JSX.Element { }); }; + const gbToBytes = (gb: number): number => Math.round(gb * 1024 ** 3); + const getFormattedTime = (date: string): string => dayjs(date).format('MMM DD,YYYY, hh:mm a'); @@ -405,10 +409,10 @@ function MultiIngestionSettings(): JSX.Element { signal: signalName, config: { day: { - size: parseInt(dailyLimit, 10) * BYTES, + size: gbToBytes(dailyLimit), }, second: { - size: parseInt(secondsLimit, 10) * BYTES, + size: gbToBytes(secondsLimit), }, }, }; @@ -427,17 +431,17 @@ function MultiIngestionSettings(): JSX.Element { signal: signal.signal, config: { day: { - size: parseInt(dailyLimit, 10) * BYTES, + size: gbToBytes(dailyLimit), }, second: { - size: parseInt(secondsLimit, 10) * BYTES, + size: gbToBytes(secondsLimit), }, }, }; updateLimitForIngestionKey(payload); }; - const getFormattedLimit = (size: number | undefined): number => { + const bytesToGb = (size: number | undefined): number => { if (!size) { return 0; } @@ -453,8 +457,8 @@ function MultiIngestionSettings(): JSX.Element { setActiveSignal(signal); addEditLimitForm.setFieldsValue({ - dailyLimit: getFormattedLimit(signal?.config?.day?.size), - secondsLimit: getFormattedLimit(signal?.config?.second?.size), + dailyLimit: bytesToGb(signal?.config?.day?.size || 0), + secondsLimit: bytesToGb(signal?.config?.second?.size || 0), }); setIsEditAddLimitOpen(true); @@ -663,12 +667,8 @@ function MultiIngestionSettings(): JSX.Element { form={addEditLimitForm} autoComplete="off" initialValues={{ - dailyLimit: getFormattedLimit( - limits[signal]?.config?.day?.size, - ), - secondsLimit: getFormattedLimit( - limits[signal]?.config?.second?.size, - ), + dailyLimit: bytesToGb(limits[signal]?.config?.day?.size), + secondsLimit: bytesToGb(limits[signal]?.config?.second?.size), }} className="edit-ingestion-key-limit-form" > @@ -683,15 +683,13 @@ function MultiIngestionSettings(): JSX.Element {
- - - - - + } /> @@ -710,15 +708,13 @@ function MultiIngestionSettings(): JSX.Element {
- - - - - + } /> @@ -796,8 +792,15 @@ function MultiIngestionSettings(): JSX.Element {
{limits[signal]?.config?.day?.size ? ( <> - {getFormattedLimit(limits[signal]?.metric?.day?.size)} GB /{' '} - {getFormattedLimit(limits[signal]?.config?.day?.size)} GB + {getYAxisFormattedValue( + (limits[signal]?.metric?.day?.size || 0).toString(), + 'bytes', + )}{' '} + /{' '} + {getYAxisFormattedValue( + (limits[signal]?.config?.day?.size || 0).toString(), + 'bytes', + )} ) : ( <> @@ -815,9 +818,15 @@ function MultiIngestionSettings(): JSX.Element {
{limits[signal]?.config?.second?.size ? ( <> - {getFormattedLimit(limits[signal]?.metric?.second?.size)} GB - / {getFormattedLimit(limits[signal]?.config?.second?.size)}{' '} - GB + {getYAxisFormattedValue( + (limits[signal]?.metric?.second?.size || 0).toString(), + 'bytes', + )}{' '} + /{' '} + {getYAxisFormattedValue( + (limits[signal]?.config?.second?.size || 0).toString(), + 'bytes', + )} ) : ( <> diff --git a/frontend/src/pages/Settings/utils.ts b/frontend/src/pages/Settings/utils.ts index cb2a5869676..9dda5632e6b 100644 --- a/frontend/src/pages/Settings/utils.ts +++ b/frontend/src/pages/Settings/utils.ts @@ -27,7 +27,7 @@ export const getRoutes = ( } if (isCloudUser()) { - if (!isGatewayEnabled) { + if (isGatewayEnabled) { settings.push(...multiIngestionSettings(t)); } else { settings.push(...ingestionSettings(t)); From 3b3e564196835445bde0f0622c10f3dfc249a35c Mon Sep 17 00:00:00 2001 From: Yunus A M Date: Tue, 4 Jun 2024 18:01:18 +0530 Subject: [PATCH 13/14] feat: enable multiple ingestion keys for all users with gateway enabled --- frontend/src/components/Tags/Tags.styles.scss | 2 +- .../IngestionSettings.styles.scss | 14 ++++++++++++++ frontend/src/pages/Settings/utils.ts | 18 ++++++++---------- frontend/src/periscope.scss | 6 +++--- frontend/src/utils/app.ts | 2 -- 5 files changed, 26 insertions(+), 16 deletions(-) diff --git a/frontend/src/components/Tags/Tags.styles.scss b/frontend/src/components/Tags/Tags.styles.scss index f2abbf07a20..1990b162694 100644 --- a/frontend/src/components/Tags/Tags.styles.scss +++ b/frontend/src/components/Tags/Tags.styles.scss @@ -17,7 +17,7 @@ .ant-tag { margin-right: 0; - background: white; + background: var(--bg-vanilla-100); } } diff --git a/frontend/src/container/IngestionSettings/IngestionSettings.styles.scss b/frontend/src/container/IngestionSettings/IngestionSettings.styles.scss index 60eb8cd027b..2c77750cd4c 100644 --- a/frontend/src/container/IngestionSettings/IngestionSettings.styles.scss +++ b/frontend/src/container/IngestionSettings/IngestionSettings.styles.scss @@ -866,6 +866,10 @@ .user-email { background: var(--bg-vanilla-200); } + + .limits-data { + border: 1px solid var(--bg-vanilla-300); + } } .ingestion-key-modal { @@ -925,4 +929,14 @@ .copyable-text { background: var(--bg-vanilla-200); } + + .ingestion-key-expires-at { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-200); + box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2); + } + + .expires-at .ant-picker { + border-color: var(--bg-vanilla-300) !important; + } } diff --git a/frontend/src/pages/Settings/utils.ts b/frontend/src/pages/Settings/utils.ts index 9dda5632e6b..4d54c05603d 100644 --- a/frontend/src/pages/Settings/utils.ts +++ b/frontend/src/pages/Settings/utils.ts @@ -26,18 +26,16 @@ export const getRoutes = ( settings.push(...organizationSettings(t)); } - if (isCloudUser()) { - if (isGatewayEnabled) { - settings.push(...multiIngestionSettings(t)); - } else { - settings.push(...ingestionSettings(t)); - } - - settings.push(...alertChannels(t)); - } else { - settings.push(...alertChannels(t)); + if (isGatewayEnabled && userRole === USER_ROLES.ADMIN) { + settings.push(...multiIngestionSettings(t)); } + if (isCloudUser() && !isGatewayEnabled) { + settings.push(...ingestionSettings(t)); + } + + settings.push(...alertChannels(t)); + if ((isCloudUser() || isEECloudUser()) && userRole === USER_ROLES.ADMIN) { settings.push(...apiKeys(t)); } diff --git a/frontend/src/periscope.scss b/frontend/src/periscope.scss index 097f22f7c1c..93f3fc73eda 100644 --- a/frontend/src/periscope.scss +++ b/frontend/src/periscope.scss @@ -27,8 +27,8 @@ cursor: pointer; &.primary { - color: #fff; - background-color: var(--bg-robin-500, #4E74F8); + color: var(--bg-vanilla-100) !important; + background-color: var(--bg-robin-500) !important; border: none; box-shadow: 0 2px 0 rgba(62, 86, 245, 0.09); } @@ -50,4 +50,4 @@ background: var(--bg-vanilla-100); color: var(--bg-ink-200); } -} \ No newline at end of file +} diff --git a/frontend/src/utils/app.ts b/frontend/src/utils/app.ts index fa486768357..fdc95ee471f 100644 --- a/frontend/src/utils/app.ts +++ b/frontend/src/utils/app.ts @@ -15,8 +15,6 @@ export function extractDomain(email: string): string { export const isCloudUser = (): boolean => { const { hostname } = window.location; - return true; - return hostname?.endsWith('signoz.cloud'); }; From 66a7bd702fcfd9afcc47c411bd5966265feba99f Mon Sep 17 00:00:00 2001 From: Yunus A M Date: Tue, 4 Jun 2024 18:04:20 +0530 Subject: [PATCH 14/14] chore: remove yarn.lock --- yarn.lock | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 yarn.lock diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index fb57ccd13af..00000000000 --- a/yarn.lock +++ /dev/null @@ -1,4 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - -