diff --git a/src/@types/parseable/api/stream.ts b/src/@types/parseable/api/stream.ts index 2db7fed5..be769182 100644 --- a/src/@types/parseable/api/stream.ts +++ b/src/@types/parseable/api/stream.ts @@ -44,3 +44,14 @@ export type StreamInfo = { } export type LogStreamRetention = Array; + +export type HotTierConfig = { + size: string; + used_size: string; + available_size: string; + oldest_date_time_entry: string; +} | {}; + +export type UpdateHotTierConfig = { + size: string; +} \ No newline at end of file diff --git a/src/api/constants.ts b/src/api/constants.ts index 00327e63..de47f721 100644 --- a/src/api/constants.ts +++ b/src/api/constants.ts @@ -14,6 +14,7 @@ export const LOG_STREAMS_STATS_URL = (streamName: string) => `${LOG_STREAM_LIST_ export const LOG_STREAMS_INFO_URL = (streamName: string) => `${LOG_STREAM_LIST_URL}/${streamName}/info`; export const DELETE_STREAMS_URL = (streamName: string) => `${LOG_STREAM_LIST_URL}/${streamName}`; export const CREATE_STREAM_URL = (streamName: string) => `${LOG_STREAM_LIST_URL}/${streamName}`; +export const LOG_STREAM_HOT_TIER = (streamName: string) => `${LOG_STREAM_LIST_URL}/${streamName}/hottier`; // About Parsable Instance export const ABOUT_URL = `${API_V1}/about`; diff --git a/src/api/logStream.ts b/src/api/logStream.ts index 09c8c4cb..708ab689 100644 --- a/src/api/logStream.ts +++ b/src/api/logStream.ts @@ -12,9 +12,10 @@ import { LIST_SAVED_FILTERS_URL, UPDATE_SAVED_FILTERS_URL, DELETE_SAVED_FILTERS_URL, - CREATE_SAVED_FILTERS_URL + CREATE_SAVED_FILTERS_URL, + LOG_STREAM_HOT_TIER } from './constants'; -import { LogStreamData, LogStreamSchemaData } from '@/@types/parseable/api/stream'; +import { HotTierConfig, LogStreamData, LogStreamSchemaData } from '@/@types/parseable/api/stream'; export const getLogStreamList = () => { return Axios().get(LOG_STREAM_LIST_URL); @@ -75,3 +76,15 @@ export const updateLogStream = (streamName: string, data: any, headers: any) => export const getLogStreamInfo = (streamName: string) => { return Axios().get(LOG_STREAMS_INFO_URL(streamName)); }; + +export const getHotTierInfo = (streamName: string) => { + return Axios().get(LOG_STREAM_HOT_TIER(streamName)); +} + +export const updateHotTierInfo = (streamName: string, data: any) => { + return Axios().put(LOG_STREAM_HOT_TIER(streamName), data); +}; + +export const deleteHotTierInfo = (streamName: string) => { + return Axios().delete(LOG_STREAM_HOT_TIER(streamName)); +}; \ No newline at end of file diff --git a/src/hooks/useHotTier.ts b/src/hooks/useHotTier.ts new file mode 100644 index 00000000..6c2e82c8 --- /dev/null +++ b/src/hooks/useHotTier.ts @@ -0,0 +1,71 @@ +import { useMutation, useQuery } from 'react-query'; +import { notifyError, notifySuccess } from '@/utils/notification'; +import { useStreamStore, streamStoreReducers } from '@/pages/Stream/providers/StreamProvider'; +import { deleteHotTierInfo, getHotTierInfo, updateHotTierInfo } from '@/api/logStream'; +import { AxiosError, isAxiosError } from 'axios'; + +const { setHotTier } = streamStoreReducers; + +export const useHotTier = (streamName: string) => { + const [, setStreamStore] = useStreamStore((_store) => null); + const { + refetch: refetchHotTierInfo, + isError: getHotTierInfoError, + isLoading: getHotTierInfoLoading, + } = useQuery(['fetch-hot-tier-info', streamName], () => getHotTierInfo(streamName), { + retry: false, + enabled: streamName !== '', + refetchOnWindowFocus: false, + onSuccess: (data) => { + setStreamStore((store) => setHotTier(store, data.data)); + }, + onError: () => setStreamStore((store) => setHotTier(store, {})), + }); + + const { mutate: updateHotTier, isLoading: isUpdating } = useMutation( + ({ size }: { size: string; onSuccess?: () => void }) => updateHotTierInfo(streamName, { size }), + { + onSuccess: (_data, variables) => { + notifySuccess({ message: `Hot tier size modified successfully` }); + refetchHotTierInfo(); + variables.onSuccess && variables.onSuccess(); + }, + onError: (data: AxiosError) => { + if (isAxiosError(data) && data.response) { + const error = data.response?.data as string; + typeof error === 'string' && notifyError({ message: error }); + } else if (data.message && typeof data.message === 'string') { + notifyError({ message: data.message, autoClose: 5000 }); + } + }, + }, + ); + + const { mutate: deleteHotTier, isLoading: isDeleting } = useMutation( + (_opts: { onSuccess?: () => void }) => deleteHotTierInfo(streamName), + { + onSuccess: (_data, variables) => { + notifySuccess({ message: `Hot tier config deleted successfully` }); + refetchHotTierInfo(); + variables.onSuccess && variables.onSuccess(); + }, + onError: (data: AxiosError) => { + if (isAxiosError(data) && data.response) { + const error = data.response?.data as string; + typeof error === 'string' && notifyError({ message: error }); + } else if (data.message && typeof data.message === 'string') { + notifyError({ message: data.message, autoClose: 5000 }); + } + }, + }, + ); + + return { + getHotTierInfoError, + getHotTierInfoLoading, + updateHotTier, + deleteHotTier, + isDeleting, + isUpdating, + }; +}; diff --git a/src/pages/Stream/Views/Manage/Management.tsx b/src/pages/Stream/Views/Manage/Management.tsx index 7dc8799e..b6f6a2dd 100644 --- a/src/pages/Stream/Views/Manage/Management.tsx +++ b/src/pages/Stream/Views/Manage/Management.tsx @@ -8,8 +8,8 @@ import { useLogStreamStats } from '@/hooks/useLogStreamStats'; import Info from './Info'; import DeleteStreamModal from '../../components/DeleteStreamModal'; import { useRetentionQuery } from '@/hooks/useRetentionEditor'; -import { useCacheToggle } from '@/hooks/useCacheToggle'; import { useGetStreamInfo } from '@/hooks/useGetStreamInfo'; +import { useHotTier } from '@/hooks/useHotTier'; const Management = (props: { schemaLoading: boolean }) => { const [currentStream] = useAppStore((store) => store.currentStream); @@ -17,14 +17,16 @@ const Management = (props: { schemaLoading: boolean }) => { const getStreamAlertsConfig = useAlertsQuery(currentStream || ''); const getStreamStats = useLogStreamStats(currentStream || ''); const getRetentionConfig = useRetentionQuery(currentStream || ''); - const { getCacheError, updateCacheStatus } = useCacheToggle(currentStream || ''); const getStreamInfo = useGetStreamInfo(currentStream || ''); + const hotTierFetch = useHotTier(currentStream || '') // todo - handle loading and error states separately const isStatsLoading = getStreamStats.getLogStreamStatsDataIsLoading || getStreamStats.getLogStreamStatsDataIsError; const isAlertsLoading = getStreamAlertsConfig.isError || getStreamAlertsConfig.isLoading; - const isSettingsLoading = getRetentionConfig.getLogRetentionIsLoading || getRetentionConfig.getLogRetentionIsError || instanceConfig === null; + const isRetentionLoading = getRetentionConfig.getLogRetentionIsLoading || getRetentionConfig.getLogRetentionIsError || instanceConfig === null; const isStreamInfoLoading = getStreamInfo.getStreamInfoLoading || getStreamInfo.getStreamInfoError; + const isHotTierLoading = hotTierFetch.getHotTierInfoLoading; + return ( @@ -35,10 +37,12 @@ const Management = (props: { schemaLoading: boolean }) => { { return ( - + Settings ); @@ -104,14 +105,194 @@ const RetentionForm = (props: { updateRetentionConfig: ({ config }: { config: an ); }; +function extractNumber(value: string | null) { + if (_.isEmpty(value) || value === null) return 0; + + const regex = /^(\d+)/; + const match = value.match(regex); + return match ? parseFloat(match[0]) : 0; +} + +const DeleteHotTierModal = (props: { + deleteHotTierInfo: ({ onSuccess }: { onSuccess: () => void }) => void; + isDeleting: boolean; + closeModal: () => void; + showDeleteModal: boolean; +}) => { + const [currentStream] = useAppStore((store) => store.currentStream); + + const onDelete = useCallback(() => { + props.deleteHotTierInfo({ onSuccess: props.closeModal }); + }, []); + + return ( + Delete Hot Tier}> + + + + Are you sure want to reset hot tier config and cached data for {currentStream} ? + + + + + + + + + + + + + ); +}; + +const HotTierConfig = (props: { + updateHotTierInfo: ({ size }: { size: string }) => void; + deleteHotTierInfo: ({ onSuccess }: { onSuccess: () => void }) => void; + isDeleting: boolean; + isUpdating: boolean; +}) => { + const [hotTier] = useStreamStore((store) => store.hotTier); + const size = _.get(hotTier, 'size', ''); + const usedSize = _.get(hotTier, 'used_size', ''); + const availableSize = _.get(hotTier, 'available_size', ''); + const oldestEntry = _.get(hotTier, 'oldest_date_time_entry', ''); + const sanitizedSize = extractNumber(size); + const [localSizeValue, setLocalSizeValue] = useState(sanitizedSize); + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [isDirty, setIsDirty] = useState(false); + + const onChangeHandler = useCallback((e: string | number) => { + setLocalSizeValue(_.toNumber(e)); + }, []); + + const onCancel = useCallback(() => { + setLocalSizeValue(sanitizedSize); + }, [sanitizedSize]); + + useEffect(() => { + setIsDirty(sanitizedSize !== localSizeValue); + }, [localSizeValue]); + + useEffect(() => { + setLocalSizeValue(sanitizedSize); + setIsDirty(sanitizedSize !== localSizeValue); + }, [hotTier]); + + const onUpdate = useCallback(() => { + props.updateHotTierInfo({ size: `${localSizeValue}GiB` }); + }, [localSizeValue]); + + const hotTierNotSet = _.isEmpty(size) || _.isEmpty(hotTier); + const closeDeleteModal = useCallback(() => { + return setShowDeleteModal(false); + }, []); + const openDeleteModal = useCallback(() => { + return setShowDeleteModal(true); + }, []); + + return ( + + + + Hot Tier Storage Size + {!hotTierNotSet && ( + + )} + + + + Oldest Record: + + {_.isEmpty(oldestEntry) ? 'No Entries Stored' : new Date(oldestEntry + ' UTC').toLocaleString()} + + + + + + + {usedSize} used | {availableSize} available + + + + + + + + + + {props.isUpdating && } + + + + + + + + + + ); +}; + const Settings = (props: { isLoading: boolean; - getCacheError: boolean; - updateCacheStatus: ({ type }: { type: boolean }) => void; updateRetentionConfig: ({ config }: { config: any }) => void; + updateHotTierInfo: ({ size }: { size: string }) => void; + deleteHotTierInfo: ({ onSuccess }: { onSuccess: () => void }) => void; + isDeleting: boolean; + isUpdating: boolean; }) => { const [isStandAloneMode] = useAppStore((store) => store.isStandAloneMode); - const [cacheEnabled] = useStreamStore((store) => store.cacheEnabled); return (
@@ -124,36 +305,15 @@ const Settings = (props: { ) : ( <> - - Caching - - {props.getCacheError ? ( - Global cache not set - ) : ( - _.isBoolean(cacheEnabled) && ( - props.updateCacheStatus({ type: event.currentTarget.checked })} - label={cacheEnabled ? 'Enabled' : 'Disabled'} - classNames={{ label: classes.fieldDescription }} - /> - ) - )} - - + {!isStandAloneMode && ( + + )} + Retention diff --git a/src/pages/Stream/providers/StreamProvider.tsx b/src/pages/Stream/providers/StreamProvider.tsx index d9213c7e..10a7608e 100644 --- a/src/pages/Stream/providers/StreamProvider.tsx +++ b/src/pages/Stream/providers/StreamProvider.tsx @@ -1,4 +1,4 @@ -import { LogStreamSchemaData, LogStreamStat, StreamInfo } from '@/@types/parseable/api/stream'; +import { HotTierConfig, LogStreamSchemaData, LogStreamStat, StreamInfo } from '@/@types/parseable/api/stream'; import initContext from '@/utils/initContext'; import _ from 'lodash'; import { AxiosResponse } from 'axios'; @@ -83,6 +83,7 @@ type StreamStore = { description: string; }; alertsConfig: TransformedAlerts; + hotTier: HotTierConfig; info: StreamInfo | {}; sideBarOpen: boolean; cacheEnabled: boolean | null; @@ -100,6 +101,7 @@ type LogsStoreReducers = { toggleSideBar: (store: StreamStore) => ReducerOutput; setCacheEnabled: (store: StreamStore, enabled: boolean) => ReducerOutput; setStreamInfo: (_store: StreamStore, infoResponse: AxiosResponse) => ReducerOutput; + setHotTier: (_store: StreamStore, hotTier: HotTierConfig) => ReducerOutput; }; const initialState: StreamStore = { @@ -118,7 +120,8 @@ const initialState: StreamStore = { }, info: {}, sideBarOpen: false, - cacheEnabled: null + cacheEnabled: null, + hotTier: {} }; const { Provider: StreamProvider, useStore: useStreamStore } = initContext(initialState); @@ -214,6 +217,12 @@ const setStreamInfo = (_store: StreamStore, infoResponse: AxiosResponse { + return { + hotTier + } +} + const operatorLabelMap = { lessThanEquals: '<=', greaterThanEquals: '>=', @@ -328,7 +337,8 @@ const streamStoreReducers: LogsStoreReducers = { setStats, toggleSideBar, setCacheEnabled, - setStreamInfo + setStreamInfo, + setHotTier }; export { StreamProvider, useStreamStore, streamStoreReducers }; diff --git a/src/pages/Stream/styles/Management.module.css b/src/pages/Stream/styles/Management.module.css index cd0a3fa3..638b98ab 100644 --- a/src/pages/Stream/styles/Management.module.css +++ b/src/pages/Stream/styles/Management.module.css @@ -148,3 +148,23 @@ color: var(--mantine-color-brandPrimary-4); cursor: pointer; } + +.actionIconCheck { + border-radius: 0.2rem; + color: white; + background-color: var(--mantine-color-brandPrimary-4); + cursor: pointer; +} + +.actionIconClose { + border-radius: 0.2rem; + color: var(--mantine-color-brandPrimary-4); + border: 1px solid var(--mantine-color-brandPrimary-4); + border-color: var(--mantine-color-brandPrimary-4); + cursor: pointer; +} + +.deleteIcon { + color: var(--mantine-color-gray-6); + cursor: pointer; +} \ No newline at end of file