diff --git a/frontend/package.json b/frontend/package.json index 918c2ed416..ee32d3dee7 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 0000000000..256e88391a --- /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 0000000000..58ebf8a0d9 --- /dev/null +++ b/frontend/public/locales/en/ingestionKeys.json @@ -0,0 +1,4 @@ +{ + "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/ErrorResponseHandler.ts b/frontend/src/api/ErrorResponseHandler.ts index 027418ec84..be2dd5e31a 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 0000000000..77556ed20a --- /dev/null +++ b/frontend/src/api/IngestionKeys/createIngestionKey.ts @@ -0,0 +1,29 @@ +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 { + 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 0000000000..5f4e7e02c7 --- /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 0000000000..e202917445 --- /dev/null +++ b/frontend/src/api/IngestionKeys/getAllIngestionKeys.ts @@ -0,0 +1,21 @@ +import { GatewayApiV1Instance } from 'api'; +import { AxiosResponse } from 'axios'; +import { + AllIngestionKeyProps, + GetIngestionKeyProps, +} from 'types/api/ingestionKeys/types'; + +export const getAllIngestionKeys = ( + props: GetIngestionKeyProps, +): 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/api/IngestionKeys/limits/createLimitsForKey.ts b/frontend/src/api/IngestionKeys/limits/createLimitsForKey.ts new file mode 100644 index 0000000000..75128b9b78 --- /dev/null +++ b/frontend/src/api/IngestionKeys/limits/createLimitsForKey.ts @@ -0,0 +1,65 @@ +/* eslint-disable @typescript-eslint/no-throw-literal */ +import { GatewayApiV1Instance } from 'api'; +import axios from 'axios'; +import { + AddLimitProps, + 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: AddLimitProps, +): Promise | ErrorResponse> => { + 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)) { + // 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 0000000000..c0b3480c45 --- /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 0000000000..89f3031e08 --- /dev/null +++ b/frontend/src/api/IngestionKeys/limits/updateLimitsForIngestionKey.ts @@ -0,0 +1,65 @@ +/* eslint-disable @typescript-eslint/no-throw-literal */ +import { GatewayApiV1Instance } from 'api'; +import axios from 'axios'; +import { + LimitSuccessProps, + UpdateLimitProps, +} 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: UpdateLimitProps, +): Promise | ErrorResponse> => { + try { + const response = await GatewayApiV1Instance.patch( + `/workspaces/me/limits/${props.limitID}`, + { + config: props.config, + }, + ); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; + } catch (error) { + if (axios.isAxiosError(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/api/IngestionKeys/updateIngestionKey.ts b/frontend/src/api/IngestionKeys/updateIngestionKey.ts new file mode 100644 index 0000000000..c4777ef97f --- /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 4fba137e18..05b4e62e78 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 92a06363a1..1ec4cda601 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 0000000000..1990b16269 --- /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: var(--bg-vanilla-100); + } +} + +.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 0000000000..ac38e0e58c --- /dev/null +++ b/frontend/src/components/Tags/Tags.tsx @@ -0,0 +1,138 @@ +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) + } + onPressEnterHandler={handleInputConfirm} + /> + +
+
+
+ )} + + {!inputVisible && ( + + )} +
+ ); +} + +interface AddTagsProps { + tags: string[]; + setTags: Dispatch>; +} + +export default Tags; diff --git a/frontend/src/constants/features.ts b/frontend/src/constants/features.ts index 67f9fe6110..bb905d0d69 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/APIKeys/APIKeys.tsx b/frontend/src/container/APIKeys/APIKeys.tsx index 933012a00d..843b326649 100644 --- a/frontend/src/container/APIKeys/APIKeys.tsx +++ b/frontend/src/container/APIKeys/APIKeys.tsx @@ -68,7 +68,7 @@ type ExpiryOption = { label: string; }; -const EXPIRATION_WITHIN_SEVEN_DAYS = 7; +export const EXPIRATION_WITHIN_SEVEN_DAYS = 7; const API_KEY_EXPIRY_OPTIONS: ExpiryOption[] = [ { value: '1', label: '1 day' }, @@ -79,6 +79,25 @@ const API_KEY_EXPIRY_OPTIONS: ExpiryOption[] = [ { value: '0', label: 'No Expiry' }, ]; +export const isExpiredToken = (expiryTimestamp: number): boolean => { + if (expiryTimestamp === 0) { + return false; + } + const currentTime = dayjs(); + const tokenExpiresAt = dayjs.unix(expiryTimestamp); + return tokenExpiresAt.isBefore(currentTime); +}; + +export const getDateDifference = ( + createdTimestamp: number, + expiryTimestamp: number, +): number => { + const differenceInSeconds = Math.abs(expiryTimestamp - createdTimestamp); + + // Convert seconds to days + return differenceInSeconds / (60 * 60 * 24); +}; + function APIKeys(): JSX.Element { const { user } = useSelector((state) => state.app); const { notifications } = useNotifications(); @@ -311,25 +330,6 @@ function APIKeys(): JSX.Element { hideAddViewModal(); }; - const getDateDifference = ( - createdTimestamp: number, - expiryTimestamp: number, - ): number => { - const differenceInSeconds = Math.abs(expiryTimestamp - createdTimestamp); - - // Convert seconds to days - return differenceInSeconds / (60 * 60 * 24); - }; - - const isExpiredToken = (expiryTimestamp: number): boolean => { - if (expiryTimestamp === 0) { - return false; - } - const currentTime = dayjs(); - const tokenExpiresAt = dayjs.unix(expiryTimestamp); - return tokenExpiresAt.isBefore(currentTime); - }; - const columns: TableProps['columns'] = [ { title: 'API Key', diff --git a/frontend/src/container/IngestionSettings/IngestionSettings.styles.scss b/frontend/src/container/IngestionSettings/IngestionSettings.styles.scss index 3d5f41ab33..2c77750cd4 100644 --- a/frontend/src/container/IngestionSettings/IngestionSettings.styles.scss +++ b/frontend/src/container/IngestionSettings/IngestionSettings.styles.scss @@ -1,3 +1,942 @@ .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-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-tags-container { + display: flex; + 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; + + .edit-ingestion-key-limit-form { + width: 100%; + } + + .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; + } + + .limit-value { + display: flex; + align-items: center; + gap: 8px; + + font-size: 12px; + font-weight: 600; + } + } + } + + .signal-limit-edit-mode { + display: flex; + justify-content: space-between; + gap: 16px; + } + } + + .signal-limit-save-discard { + display: flex; + gap: 8px; + } + } + } + } +} + +.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% */ + } +} + +.expires-at { + .ant-picker { + border-color: var(--bg-slate-400) !important; + } +} + +.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; + } +} + +.ingestion-key-details-edit-drawer-title { + display: flex; + gap: 8px; +} + +.ingestion-key-details-meta { + padding: 14px 16px; + border-radius: 3px; + border: 1px solid var(--Slate-500, #161922); +} + +#edit-ingestion-key-form { + .ant-form-item:last-child { + margin-bottom: 0px; + } +} + +.alert { + display: flex; + gap: 12px; + + padding: 8px; + margin: 16px 0; + + border-radius: 4px; + background: rgba(113, 144, 249, 0.1); + color: var(--Robin-300, #95acfb); + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: 160%; + letter-spacing: 0.013px; +} + +.error { + color: var(--bg-cherry-500); + margin-bottom: 8px; +} + +.save-discard-changes { + display: flex; + gap: 8px; + justify-content: flex-end; +} + +.ingestion-details-edit-drawer { + .ant-drawer-header { + border-bottom: 1px solid var(--Slate-500, #161922); + padding: 8px; + } + + .ant-drawer-footer { + border-top: 1px solid var(--Slate-500, #161922); + padding: 8px; + } +} + +.ingestion-key-limits { + margin-top: 48px; + padding: 16px; + border-radius: 3px; + border: 1px solid var(--Slate-500, #161922); + + .ant-tabs { + .ant-tabs-nav { + margin-top: -36px; + + &::before { + border-bottom: none; + } + + .ant-tabs-nav-list { + background: #121317; + border-radius: 2px; + border: 1px solid var(--Slate-400, #1d212d); + background: var(--Ink-400, #121317); + box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1); + + .ant-tabs-tab { + display: inline-flex; + padding: 6px 36px; + justify-content: center; + align-items: center; + margin: 0px; + border-right: 1px solid #1d212d; + + &.ant-tabs-tab-active { + border-bottom: 0px; + } + + .tab-name { + display: flex; + align-items: center; + gap: 8px; + } + } + } + } + } +} + +.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); +} + +.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); + } + + .limits-data { + border: 1px solid var(--bg-vanilla-300); + } + } + + .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); + } + + .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/container/IngestionSettings/MultiIngestionSettings.tsx b/frontend/src/container/IngestionSettings/MultiIngestionSettings.tsx new file mode 100644 index 0000000000..7d704f0432 --- /dev/null +++ b/frontend/src/container/IngestionSettings/MultiIngestionSettings.tsx @@ -0,0 +1,1142 @@ +import './IngestionSettings.styles.scss'; + +import { Color } from '@signozhq/design-tokens'; +import { + Button, + Col, + Collapse, + DatePicker, + Form, + Input, + InputNumber, + 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 { getYAxisFormattedValue } from 'components/Graph/yAxisConfig'; +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 gbToBytes = (gb: number): number => Math.round(gb * 1024 ** 3); + + 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: gbToBytes(dailyLimit), + }, + second: { + size: gbToBytes(secondsLimit), + }, + }, + }; + + 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: gbToBytes(dailyLimit), + }, + second: { + size: gbToBytes(secondsLimit), + }, + }, + }; + updateLimitForIngestionKey(payload); + }; + + const bytesToGb = (size: number | undefined): number => { + if (!size) { + return 0; + } + + return size / BYTES; + }; + + const enableEditLimitMode = ( + APIKey: IngestionKeyProps, + signal: LimitProps, + ): void => { + setActiveAPIKey(APIKey); + setActiveSignal(signal); + + addEditLimitForm.setFieldsValue({ + dailyLimit: bytesToGb(signal?.config?.day?.size || 0), + secondsLimit: bytesToGb(signal?.config?.second?.size || 0), + }); + + 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 ? ( + <> + {getYAxisFormattedValue( + (limits[signal]?.metric?.day?.size || 0).toString(), + 'bytes', + )}{' '} + /{' '} + {getYAxisFormattedValue( + (limits[signal]?.config?.day?.size || 0).toString(), + 'bytes', + )} + + ) : ( + <> + NO LIMIT + + )} +
+
+ +
+
+ Seconds +
+ +
+ {limits[signal]?.config?.second?.size ? ( + <> + {getYAxisFormattedValue( + (limits[signal]?.metric?.second?.size || 0).toString(), + 'bytes', + )}{' '} + /{' '} + {getYAxisFormattedValue( + (limits[signal]?.config?.second?.size || 0).toString(), + 'bytes', + )} + + ) : ( + <> + NO LIMIT + + )} +
+
+
+ )} +
+
+ ))} +
+
+
+
+ ), + }, + ]; + + return ( +
+ + +
+
+ + Expires on + {formattedDateAndTime} +
+
+
+ ); + }, + }, + ]; + + const handleTableChange = (pagination: TablePaginationConfig): void => { + setPaginationParams({ + page: pagination?.current || 1, + per_page: 10, + }); + }; + + return ( +
+
+
+ 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/hooks/IngestionKeys/useGetAllIngestionKeys.ts b/frontend/src/hooks/IngestionKeys/useGetAllIngestionKeys.ts new file mode 100644 index 0000000000..8c4a19a0e9 --- /dev/null +++ b/frontend/src/hooks/IngestionKeys/useGetAllIngestionKeys.ts @@ -0,0 +1,15 @@ +import { getAllIngestionKeys } from 'api/IngestionKeys/getAllIngestionKeys'; +import { AxiosError, AxiosResponse } from 'axios'; +import { useQuery, UseQueryResult } from 'react-query'; +import { + AllIngestionKeyProps, + GetIngestionKeyProps, +} from 'types/api/ingestionKeys/types'; + +export const useGetAllIngestionsKeys = ( + props: GetIngestionKeyProps, +): UseQueryResult, AxiosError> => + useQuery, AxiosError>({ + queryKey: [`IngestionKeys-${props.page}-${props.search}`], + queryFn: () => getAllIngestionKeys(props), + }); diff --git a/frontend/src/pages/Settings/config.tsx b/frontend/src/pages/Settings/config.tsx index c6073ce322..94b06a5547 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 c2bdcef5e6..bef3d7cba8 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 6078ef7621..4d54c05603 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 = []; @@ -24,13 +26,16 @@ export const getRoutes = ( settings.push(...organizationSettings(t)); } - if (isCloudUser()) { - 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 423776d4aa..93f3fc73ed 100644 --- a/frontend/src/periscope.scss +++ b/frontend/src/periscope.scss @@ -27,8 +27,9 @@ cursor: pointer; &.primary { - color: #fff; - background-color: #4566d6; + 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); } diff --git a/frontend/src/periscope/components/Tabs/Tabs.tsx b/frontend/src/periscope/components/Tabs/Tabs.tsx new file mode 100644 index 0000000000..219a09e3f2 --- /dev/null +++ b/frontend/src/periscope/components/Tabs/Tabs.tsx @@ -0,0 +1,13 @@ +/* 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; + key: string; + children: React.ReactElement; +} + +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 new file mode 100644 index 0000000000..4b674c987b --- /dev/null +++ b/frontend/src/periscope/components/Tabs/index.tsx @@ -0,0 +1,3 @@ +import Tabs from './Tabs'; + +export default Tabs; diff --git a/frontend/src/types/api/ingestionKeys/limits/types.ts b/frontend/src/types/api/ingestionKeys/limits/types.ts new file mode 100644 index 0000000000..279ec157d5 --- /dev/null +++ b/frontend/src/types/api/ingestionKeys/limits/types.ts @@ -0,0 +1,55 @@ +export interface LimitProps { + id: string; + signal: string; + tags?: string[]; + key_id?: string; + created_at?: string; + updated_at?: string; + config?: { + day?: { + size?: number; + }; + second?: { + size?: number; + }; + }; + metric?: { + 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: { + size: number; + }; + second: { + size: number; + }; + }; +} + +export interface LimitSuccessProps { + status: string; + response: unknown; +} diff --git a/frontend/src/types/api/ingestionKeys/types.ts b/frontend/src/types/api/ingestionKeys/types.ts new file mode 100644 index 0000000000..b29c539cb1 --- /dev/null +++ b/frontend/src/types/api/ingestionKeys/types.ts @@ -0,0 +1,85 @@ +export interface User { + createdAt?: number; + email?: string; + id: string; + name?: string; + notFound?: boolean; + profilePictureURL?: string; +} + +export interface Limit { + signal: string; + id: string; + config?: { + day?: { + size?: number; + }; + second?: { + size?: number; + }; + }; + tags?: []; +} + +export interface IngestionKeyProps { + name: string; + expires_at?: string; + value: string; + workspace_id: string; + id: string; + created_at: string; + updated_at: string; + tags?: string[]; + limits?: Limit[]; +} + +export interface GetIngestionKeyProps { + page: number; + per_page: number; + search?: string; +} + +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 { + data: IngestionKeyProps; +} + +export interface DeleteIngestionKeyPayloadProps { + status: string; +} + +export interface UpdateIngestionKeyProps { + id: string; + data: { + name: string; + expires_at: string; + tags: string[]; + }; +} + +export type IngestionKeysPayloadProps = { + status: string; + data: string; +}; + +export type GetIngestionKeyPayloadProps = { + id: string; +}; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 907c2877c6..5133cfa46e 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -6755,16 +6755,16 @@ comma-separated-tokens@^2.0.0: resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== +commander@2, commander@^2.20.0, commander@^2.20.3: + version "2.20.3" + resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + commander@^10.0.0: version "10.0.1" resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== -commander@^2.20.0, commander@^2.20.3: - version "2.20.3" - resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - commander@^3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/commander/-/commander-3.0.2.tgz" @@ -7325,6 +7325,11 @@ d3-array@3.2.1: dependencies: internmap "1 - 2" +d3-array@^1.2.0: + version "1.2.4" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f" + integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw== + d3-binarytree@1: version "1.0.2" resolved "https://registry.npmjs.org/d3-binarytree/-/d3-binarytree-1.0.2.tgz" @@ -7400,6 +7405,11 @@ d3-path@1, d3-path@^1.0.5: resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf" integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg== +d3-polygon@^1.0.3: + version "1.0.6" + resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-1.0.6.tgz#0bf8cb8180a6dc107f518ddf7975e12abbfbd38e" + integrity sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ== + "d3-quadtree@1 - 3": version "3.0.1" resolved "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz" @@ -7892,7 +7902,7 @@ duplexer@^0.1.2: resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== -earcut@^2.2.3: +earcut@^2.1.1, earcut@^2.2.3: version "2.2.4" resolved "https://registry.yarnpkg.com/earcut/-/earcut-2.2.4.tgz#6d02fd4d68160c114825d06890a92ecaae60343a" integrity sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ== @@ -8891,6 +8901,18 @@ flatten-vertex-data@^1.0.0: dependencies: dtype "^2.0.0" +flubber@^0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/flubber/-/flubber-0.4.2.tgz#14452d4a838cc3b9f2fb6175da94e35acd55fbaa" + integrity sha512-79RkJe3rA4nvRCVc2uXjj7U/BAUq84TS3KHn6c0Hr9K64vhj83ZNLUziNx4pJoBumSPhOl5VjH+Z0uhi+eE8Uw== + dependencies: + d3-array "^1.2.0" + d3-polygon "^1.0.3" + earcut "^2.1.1" + svg-path-properties "^0.2.1" + svgpath "^2.2.1" + topojson-client "^3.0.0" + follow-redirects@^1.0.0, follow-redirects@^1.14.0, follow-redirects@^1.15.4: version "1.15.6" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" @@ -13310,6 +13332,11 @@ pbf@3.2.1: ieee754 "^1.1.12" resolve-protobuf-schema "^2.1.0" +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== + periscopic@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/periscopic/-/periscopic-3.1.0.tgz#7e9037bf51c5855bd33b48928828db4afa79d97a" @@ -13891,6 +13918,13 @@ raf-schd@^4.0.2: resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== +raf@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" + integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== + dependencies: + performance-now "^2.1.0" + randombytes@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz" @@ -14269,6 +14303,15 @@ rc-tree@~5.8.1, rc-tree@~5.8.2: rc-util "^5.16.1" rc-virtual-list "^3.5.1" +rc-tween-one@3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/rc-tween-one/-/rc-tween-one-3.0.6.tgz#74e09cdd9da00c27082c8b6f1606dc3bae67797b" + integrity sha512-5zTSXyyv7bahDBQ/kJw/kNxxoBqTouttoelw8FOVOyWqmTMndizJEpvaj1N+yES5Xjss6Y2iVw+9vSJQZE8Z6g== + dependencies: + "@babel/runtime" "^7.11.1" + style-utils "^0.3.4" + tween-one "^1.0.50" + rc-upload@~4.3.5: version "4.3.5" resolved "https://registry.yarnpkg.com/rc-upload/-/rc-upload-4.3.5.tgz#12fc69b2af74d08646a104828831bcaf44076eda" @@ -15990,6 +16033,11 @@ style-to-object@^0.4.0, style-to-object@^0.4.1: dependencies: inline-style-parser "0.1.1" +style-utils@^0.3.4, style-utils@^0.3.6: + version "0.3.8" + resolved "https://registry.yarnpkg.com/style-utils/-/style-utils-0.3.8.tgz#6ba4271bcc766dee4730bd51b3ef2552908dc111" + integrity sha512-RmGftIhY4tqtD1ERwKsVEDlt/M6UyxN/rcr95UmlooWmhtL0RwVUYJkpo1kSx3ppd9/JZzbknhy742zbMAawjQ== + styled-components@^5.3.11: version "5.3.11" resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-5.3.11.tgz#9fda7bf1108e39bf3f3e612fcc18170dedcd57a8" @@ -16079,6 +16127,16 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +svg-path-properties@^0.2.1: + version "0.2.2" + resolved "https://registry.yarnpkg.com/svg-path-properties/-/svg-path-properties-0.2.2.tgz#b073d81be7292eae0e233ab8a83f58dc27113296" + integrity sha512-GmrB+b6woz6CCdQe6w1GHs/1lt25l7SR5hmhF8jRdarpv/OgjLyuQygLu1makJapixeb1aQhP/Oa1iKi93o/aQ== + +svg-path-properties@^1.0.4: + version "1.3.0" + resolved "https://registry.yarnpkg.com/svg-path-properties/-/svg-path-properties-1.3.0.tgz#7f47e61dcac380c9f4d04f642df7e69b127274fa" + integrity sha512-R1+z37FrqyS3UXDhajNfvMxKI0smuVdedqOo4YbAQUfGqA86B9mGvr2IEXrwjjvGzCtdIKy/ad9N8m6YclaKAw== + svgo@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/svgo/-/svgo-3.0.2.tgz#5e99eeea42c68ee0dc46aa16da093838c262fe0a" @@ -16091,6 +16149,11 @@ svgo@^3.0.2: csso "^5.0.5" picocolors "^1.0.0" +svgpath@^2.2.1: + version "2.6.0" + resolved "https://registry.yarnpkg.com/svgpath/-/svgpath-2.6.0.tgz#5b160ef3d742b7dfd2d721bf90588d3450d7a90d" + integrity sha512-OIWR6bKzXvdXYyO4DK/UWa1VA1JeKq8E+0ug2DG98Y/vOmMpfZNj+TIG988HjfYSqtcy/hFOtZq/n/j5GSESNg== + symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz" @@ -16315,6 +16378,13 @@ toidentifier@1.0.1: resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== +topojson-client@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/topojson-client/-/topojson-client-3.1.0.tgz#22e8b1ed08a2b922feeb4af6f53b6ef09a467b99" + integrity sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw== + dependencies: + commander "2" + totalist@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz" @@ -16450,6 +16520,23 @@ tsutils@^3.21.0: dependencies: tslib "^1.8.1" +tween-functions@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/tween-functions/-/tween-functions-1.2.0.tgz#1ae3a50e7c60bb3def774eac707acbca73bbc3ff" + integrity sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA== + +tween-one@^1.0.50: + version "1.2.7" + resolved "https://registry.yarnpkg.com/tween-one/-/tween-one-1.2.7.tgz#80051e9434f145c0e31623790378af0f23fe3d00" + integrity sha512-F+Z9LO9GsYqf0j5bgNhAF98RDrAZ7QjQrujJ2lVYSHl4+dBPW/atHluL2bwclZf8Vo0Yo96f6pw2uq1OGzpC/Q== + dependencies: + "@babel/runtime" "^7.11.1" + flubber "^0.4.2" + raf "^3.4.1" + style-utils "^0.3.6" + svg-path-properties "^1.0.4" + tween-functions "^1.2.0" + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz"