chore: TTL and S3 config related changes (#1201)

* fix: 🐛 convert TTL APIs to async

* chore: add archive support

* chore: update TTL async APIs according to new design

* chore: 🔥 clean removeTTL API

* fix: metrics s3 config

* feat: ttl async with polling (#1195)

* feat: ttl state message change and time unit language changes (#1197)

* test:  update tests for async TTL api

* feat: ttl message info icon (#1202)

* feat: ttl pr review changes

* chore: refractoring

Co-authored-by: makeavish <makeavish786@gmail.com>
Co-authored-by: Pranshu Chittora <pranshu@signoz.io>
Co-authored-by: palash-signoz <palash@signoz.io>
Co-authored-by: Pranay Prateek <pranay@signoz.io>
This commit is contained in:
Prashant Shahi 2022-05-25 18:19:44 +05:30 committed by GitHub
parent f92e4798ce
commit 642c6c5920
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 560 additions and 252 deletions

View File

@ -0,0 +1,21 @@
{
"total_retention_period": "Total Retention Period",
"move_to_s3": "Move to S3\n(should be lower than total retention period)",
"status_message": {
"success": "Your last call to change retention period to {{total_retention}} {{s3_part}} was successful.",
"failed": "Your last call to change retention period to {{total_retention}} {{s3_part}} failed. Please try again.",
"pending": "Your last call to change retention period to {{total_retention}} {{s3_part}} is pending. This may take some time.",
"s3_part": "and S3 to {{s3_retention}}"
},
"retention_save_button": {
"pending": "Updating {{name}} retention period",
"success": "Save"
},
"retention_request_race_condition": "Your request to change retention period has failed, as another request is still in process.",
"retention_error_message": "There was an issue in changing the retention period for {{name}}. Please try again or reach out to support@signoz.io",
"retention_failed_message": "There was an issue in changing the retention period. Please try again or reach out to support@signoz.io",
"retention_comparison_error": "Total retention period for {{name}} cant be lower or equal to the period after which data is moved to s3.",
"retention_null_value_error": "Retention Period for {{name}} is not set yet. Please set by choosing below",
"retention_confirmation": "Are you sure you want to change the retention period?",
"retention_confirmation_description": "This will change the amount of storage needed for saving {{name}}."
}

View File

@ -13,16 +13,5 @@
"general": "General", "general": "General",
"alert_channels": "Alert Channels", "alert_channels": "Alert Channels",
"all_errors": "All Exceptions" "all_errors": "All Exceptions"
},
"settings": {
"total_retention_period": "Total Retention Period",
"move_to_s3": "Move to S3\n(should be lower than total retention period)",
"retention_success_message": "Congrats. The retention periods for {{name}} has been updated successfully.",
"retention_error_message": "There was an issue in changing the retention period for {{name}}. Please try again or reach out to support@signoz.io",
"retention_failed_message": "There was an issue in changing the retention period. Please try again or reach out to support@signoz.io",
"retention_comparison_error": "Total retention period for {{name}} cant be lower or equal to the period after which data is moved to s3.",
"retention_null_value_error": "Retention Period for {{name}} is not set yet. Please set by choosing below",
"retention_confirmation": "Are you sure you want to change the retention period?",
"retention_confirmation_description": "This will change the amount of storage needed for saving metrics & traces."
} }
} }

View File

@ -0,0 +1,21 @@
{
"total_retention_period": "Total Retention Period",
"move_to_s3": "Move to S3\n(should be lower than total retention period)",
"status_message": {
"success": "Your last call to change retention period to {{total_retention}} {{s3_part}} was successful.",
"failed": "Your last call to change retention period to {{total_retention}} {{s3_part}} failed. Please try again.",
"pending": "Your last call to change retention period to {{total_retention}} {{s3_part}} is pending. This may take some time.",
"s3_part": "and S3 to {{s3_retention}}"
},
"retention_save_button": {
"pending": "Updating {{name}} retention period",
"success": "Save"
},
"retention_request_race_condition": "Your request to change retention period has failed, as another request is still in process.",
"retention_error_message": "There was an issue in changing the retention period for {{name}}. Please try again or reach out to support@signoz.io",
"retention_failed_message": "There was an issue in changing the retention period. Please try again or reach out to support@signoz.io",
"retention_comparison_error": "Total retention period for {{name}} cant be lower or equal to the period after which data is moved to s3.",
"retention_null_value_error": "Retention Period for {{name}} is not set yet. Please set by choosing below",
"retention_confirmation": "Are you sure you want to change the retention period?",
"retention_confirmation_description": "This will change the amount of storage needed for saving {{name}}."
}

View File

@ -13,16 +13,5 @@
"general": "General", "general": "General",
"alert_channels": "Alert Channels", "alert_channels": "Alert Channels",
"all_errors": "All Exceptions" "all_errors": "All Exceptions"
},
"settings": {
"total_retention_period": "Total Retention Period",
"move_to_s3": "Move to S3\n(should be lower than total retention period)",
"retention_success_message": "Congrats. The retention periods for {{name}} has been updated successfully.",
"retention_error_message": "There was an issue in changing the retention period for {{name}}. Please try again or reach out to support@signoz.io",
"retention_failed_message": "There was an issue in changing the retention period. Please try again or reach out to support@signoz.io",
"retention_comparison_error": "Total retention period for {{name}} cant be lower or equal to the period after which data is moved to s3.",
"retention_null_value_error": "Retention Period for {{name}} is not set yet. Please set by choosing below",
"retention_confirmation": "Are you sure you want to change the retention period?",
"retention_confirmation_description": "This will change the amount of storage needed for saving metrics & traces."
} }
} }

View File

@ -2,13 +2,15 @@ import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api'; import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps } from 'types/api/settings/getRetention'; import { PayloadProps, Props } from 'types/api/settings/getRetention';
const getRetention = async (): Promise< const getRetention = async <T extends Props>(
SuccessResponse<PayloadProps> | ErrorResponse props: T,
> => { ): Promise<SuccessResponse<PayloadProps<T>> | ErrorResponse> => {
try { try {
const response = await axios.get<PayloadProps>(`/settings/ttl`); const response = await axios.get<PayloadProps<T>>(
`/settings/ttl?type=${props}`,
);
return { return {
statusCode: 200, statusCode: 200,

View File

@ -1,35 +1,67 @@
import { Button, Col, Modal, notification, Row, Typography } from 'antd'; import { LoadingOutlined } from '@ant-design/icons';
import {
Button,
Col,
Divider,
Modal,
notification,
Row,
Spin,
Typography,
} from 'antd';
import setRetentionApi from 'api/settings/setRetention'; import setRetentionApi from 'api/settings/setRetention';
import TextToolTip from 'components/TextToolTip'; import TextToolTip from 'components/TextToolTip';
import useComponentPermission from 'hooks/useComponentPermission'; import useComponentPermission from 'hooks/useComponentPermission';
import find from 'lodash-es/find'; import find from 'lodash-es/find';
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { UseQueryResult } from 'react-query';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useInterval } from 'react-use';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { import {
IDiskType, IDiskType,
PayloadProps as GetDisksPayload, PayloadProps as GetDisksPayload,
} from 'types/api/disks/getDisks'; } from 'types/api/disks/getDisks';
import { PayloadProps as GetRetentionPayload } from 'types/api/settings/getRetention'; import { TTTLType } from 'types/api/settings/common';
import {
PayloadPropsMetrics as GetRetentionPeriodMetricsPayload,
PayloadPropsTraces as GetRetentionPeriodTracesPayload,
} from 'types/api/settings/getRetention';
import AppReducer from 'types/reducer/app'; import AppReducer from 'types/reducer/app';
import Retention from './Retention'; import Retention from './Retention';
import { ButtonContainer, ErrorText, ErrorTextContainer } from './styles'; import StatusMessage from './StatusMessage';
import { ActionItemsContainer, ErrorText, ErrorTextContainer } from './styles';
type NumberOrNull = number | null; type NumberOrNull = number | null;
function GeneralSettings({ function GeneralSettings({
ttlValuesPayload, metricsTtlValuesPayload,
tracesTtlValuesPayload,
getAvailableDiskPayload, getAvailableDiskPayload,
metricsTtlValuesRefetch,
tracesTtlValuesRefetch,
}: GeneralSettingsProps): JSX.Element { }: GeneralSettingsProps): JSX.Element {
const { t } = useTranslation(); const { t } = useTranslation(['generalSettings']);
const [modal, setModal] = useState<boolean>(false); const [modalMetrics, setModalMetrics] = useState<boolean>(false);
const [postApiLoading, setPostApiLoading] = useState<boolean>(false); const [postApiLoadingMetrics, setPostApiLoadingMetrics] = useState<boolean>(
false,
);
const [postApiLoadingTraces, setPostApiLoadingTraces] = useState<boolean>(
false,
);
const [modalTraces, setModalTraces] = useState<boolean>(false);
const [availableDisks] = useState<IDiskType[]>(getAvailableDiskPayload); const [availableDisks] = useState<IDiskType[]>(getAvailableDiskPayload);
const [currentTTLValues, setCurrentTTLValues] = useState(ttlValuesPayload); const [metricsCurrentTTLValues, setMetricsCurrentTTLValues] = useState(
metricsTtlValuesPayload,
);
const [tracesCurrentTTLValues, setTracesCurrentTTLValues] = useState(
tracesTtlValuesPayload,
);
const { role } = useSelector<AppState, AppReducer>((state) => state.app); const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const [setRetentionPermission] = useComponentPermission( const [setRetentionPermission] = useComponentPermission(
@ -55,195 +87,93 @@ function GeneralSettings({
] = useState<NumberOrNull>(null); ] = useState<NumberOrNull>(null);
useEffect(() => { useEffect(() => {
if (currentTTLValues) { if (metricsCurrentTTLValues) {
setMetricsTotalRetentionPeriod(currentTTLValues.metrics_ttl_duration_hrs); setMetricsTotalRetentionPeriod(
setMetricsS3RetentionPeriod( metricsCurrentTTLValues.metrics_ttl_duration_hrs,
currentTTLValues.metrics_move_ttl_duration_hrs
? currentTTLValues.metrics_move_ttl_duration_hrs
: null,
); );
setTracesTotalRetentionPeriod(currentTTLValues.traces_ttl_duration_hrs); setMetricsS3RetentionPeriod(
setTracesS3RetentionPeriod( metricsCurrentTTLValues.metrics_move_ttl_duration_hrs
currentTTLValues.traces_move_ttl_duration_hrs ? metricsCurrentTTLValues.metrics_move_ttl_duration_hrs
? currentTTLValues.traces_move_ttl_duration_hrs
: null, : null,
); );
} }
}, [currentTTLValues]); }, [metricsCurrentTTLValues]);
const onModalToggleHandler = (): void => { useEffect(() => {
setModal((modal) => !modal); if (tracesCurrentTTLValues) {
setTracesTotalRetentionPeriod(
tracesCurrentTTLValues.traces_ttl_duration_hrs,
);
setTracesS3RetentionPeriod(
tracesCurrentTTLValues.traces_move_ttl_duration_hrs
? tracesCurrentTTLValues.traces_move_ttl_duration_hrs
: null,
);
}
}, [tracesCurrentTTLValues]);
useInterval(
async (): Promise<void> => {
if (metricsTtlValuesPayload.status === 'pending') {
metricsTtlValuesRefetch();
}
},
metricsTtlValuesPayload.status === 'pending' ? 1000 : null,
);
useInterval(
async (): Promise<void> => {
if (tracesTtlValuesPayload.status === 'pending') {
tracesTtlValuesRefetch();
}
},
tracesTtlValuesPayload.status === 'pending' ? 1000 : null,
);
const onModalToggleHandler = (type: TTTLType): void => {
if (type === 'metrics') setModalMetrics((modal) => !modal);
if (type === 'traces') setModalTraces((modal) => !modal);
};
const onPostApiLoadingHandler = (type: TTTLType): void => {
if (type === 'metrics') setPostApiLoadingMetrics((modal) => !modal);
if (type === 'traces') setPostApiLoadingTraces((modal) => !modal);
}; };
const onClickSaveHandler = useCallback(() => { const onClickSaveHandler = useCallback(
if (!setRetentionPermission) { (type: TTTLType) => {
notification.error({ if (!setRetentionPermission) {
message: `Sorry you don't have permission to make these changes`, notification.error({
}); message: `Sorry you don't have permission to make these changes`,
return; });
} return;
onModalToggleHandler(); }
}, [setRetentionPermission]); onModalToggleHandler(type);
},
[setRetentionPermission],
);
const s3Enabled = useMemo( const s3Enabled = useMemo(
() => !!find(availableDisks, (disks: IDiskType) => disks?.type === 's3'), () => !!find(availableDisks, (disks: IDiskType) => disks?.type === 's3'),
[availableDisks], [availableDisks],
); );
const renderConfig = [ const [isMetricsSaveDisabled, isTracesSaveDisabled, errorText] = useMemo((): [
{ boolean,
name: 'Metrics', boolean,
retentionFields: [ string,
{ // eslint-disable-next-line sonarjs/cognitive-complexity
name: t('settings.total_retention_period'), ] => {
value: metricsTotalRetentionPeriod,
setValue: setMetricsTotalRetentionPeriod,
},
{
name: t('settings.move_to_s3'),
value: metricsS3RetentionPeriod,
setValue: setMetricsS3RetentionPeriod,
hide: !s3Enabled,
},
],
},
{
name: 'Traces',
retentionFields: [
{
name: t('settings.total_retention_period'),
value: tracesTotalRetentionPeriod,
setValue: setTracesTotalRetentionPeriod,
},
{
name: t('settings.move_to_s3'),
value: tracesS3RetentionPeriod,
setValue: setTracesS3RetentionPeriod,
hide: !s3Enabled,
},
],
},
].map((category): JSX.Element | null => {
if (
Array.isArray(category.retentionFields) &&
category.retentionFields.length > 0
) {
return (
<Col flex="40%" style={{ minWidth: 475 }} key={category.name}>
<Typography.Title level={3}>{category.name}</Typography.Title>
{category.retentionFields.map((retentionField) => (
<Retention
key={retentionField.name}
text={retentionField.name}
retentionValue={retentionField.value}
setRetentionValue={retentionField.setValue}
hide={!!retentionField.hide}
/>
))}
</Col>
);
}
return null;
});
// eslint-disable-next-line sonarjs/cognitive-complexity
const onOkHandler = async (): Promise<void> => {
try {
setPostApiLoading(true);
const apiCalls = [];
if (
!(
currentTTLValues?.metrics_move_ttl_duration_hrs ===
metricsS3RetentionPeriod &&
currentTTLValues.metrics_ttl_duration_hrs === metricsTotalRetentionPeriod
)
) {
apiCalls.push(() =>
setRetentionApi({
type: 'metrics',
totalDuration: `${metricsTotalRetentionPeriod || -1}h`,
coldStorage: s3Enabled ? 's3' : null,
toColdDuration: `${metricsS3RetentionPeriod || -1}h`,
}),
);
} else {
apiCalls.push(() => Promise.resolve(null));
}
if (
!(
currentTTLValues?.traces_move_ttl_duration_hrs ===
tracesS3RetentionPeriod &&
currentTTLValues.traces_ttl_duration_hrs === tracesTotalRetentionPeriod
)
) {
apiCalls.push(() =>
setRetentionApi({
type: 'traces',
totalDuration: `${tracesTotalRetentionPeriod || -1}h`,
coldStorage: s3Enabled ? 's3' : null,
toColdDuration: `${tracesS3RetentionPeriod || -1}h`,
}),
);
} else {
apiCalls.push(() => Promise.resolve(null));
}
const apiCallSequence = ['metrics', 'traces'];
const apiResponses = await Promise.all(apiCalls.map((api) => api()));
apiResponses.forEach((apiResponse, idx) => {
const name = apiCallSequence[idx];
if (apiResponse) {
if (apiResponse.statusCode === 200) {
notification.success({
message: 'Success!',
placement: 'topRight',
description: t('settings.retention_success_message', { name }),
});
} else {
notification.error({
message: 'Error',
description: t('settings.retention_error_message', { name }),
placement: 'topRight',
});
}
}
});
onModalToggleHandler();
setPostApiLoading(false);
} catch (error) {
notification.error({
message: 'Error',
description: t('settings.retention_failed_message'),
placement: 'topRight',
});
}
// Updates the currentTTL Values in order to avoid pushing the same values.
setCurrentTTLValues({
metrics_ttl_duration_hrs: metricsTotalRetentionPeriod || -1,
metrics_move_ttl_duration_hrs: metricsS3RetentionPeriod || -1,
traces_ttl_duration_hrs: tracesTotalRetentionPeriod || -1,
traces_move_ttl_duration_hrs: tracesS3RetentionPeriod || -1,
});
setModal(false);
};
// eslint-disable-next-line sonarjs/cognitive-complexity
const [isDisabled, errorText] = useMemo((): [boolean, string] => {
// Various methods to return dynamic error message text. // Various methods to return dynamic error message text.
const messages = { const messages = {
compareError: (name: string | number): string => compareError: (name: string | number): string =>
t('settings.retention_comparison_error', { name }), t('retention_comparison_error', { name }),
nullValueError: (name: string | number): string => nullValueError: (name: string | number): string =>
t('settings.retention_null_value_error', { name }), t('retention_null_value_error', { name }),
}; };
// Defaults to button not disabled and empty error message text. // Defaults to button not disabled and empty error message text.
let isDisabled = false; let isMetricsSaveDisabled = false;
let isTracesSaveDisabled = false;
let errorText = ''; let errorText = '';
if (s3Enabled) { if (s3Enabled) {
@ -251,19 +181,20 @@ function GeneralSettings({
(metricsTotalRetentionPeriod || metricsS3RetentionPeriod) && (metricsTotalRetentionPeriod || metricsS3RetentionPeriod) &&
Number(metricsTotalRetentionPeriod) <= Number(metricsS3RetentionPeriod) Number(metricsTotalRetentionPeriod) <= Number(metricsS3RetentionPeriod)
) { ) {
isDisabled = true; isMetricsSaveDisabled = true;
errorText = messages.compareError('metrics'); errorText = messages.compareError('metrics');
} else if ( } else if (
(tracesTotalRetentionPeriod || tracesS3RetentionPeriod) && (tracesTotalRetentionPeriod || tracesS3RetentionPeriod) &&
Number(tracesTotalRetentionPeriod) <= Number(tracesS3RetentionPeriod) Number(tracesTotalRetentionPeriod) <= Number(tracesS3RetentionPeriod)
) { ) {
isDisabled = true; isTracesSaveDisabled = true;
errorText = messages.compareError('traces'); errorText = messages.compareError('traces');
} }
} }
if (!metricsTotalRetentionPeriod || !tracesTotalRetentionPeriod) { if (!metricsTotalRetentionPeriod || !tracesTotalRetentionPeriod) {
isDisabled = true; isMetricsSaveDisabled = true;
isTracesSaveDisabled = true;
if (!metricsTotalRetentionPeriod && !tracesTotalRetentionPeriod) { if (!metricsTotalRetentionPeriod && !tracesTotalRetentionPeriod) {
errorText = messages.nullValueError('metrics and traces'); errorText = messages.nullValueError('metrics and traces');
} else if (!metricsTotalRetentionPeriod) { } else if (!metricsTotalRetentionPeriod) {
@ -273,25 +204,240 @@ function GeneralSettings({
} }
} }
if ( if (
currentTTLValues?.metrics_ttl_duration_hrs === metricsTotalRetentionPeriod && metricsCurrentTTLValues?.metrics_ttl_duration_hrs ===
currentTTLValues.metrics_move_ttl_duration_hrs === metricsTotalRetentionPeriod &&
metricsS3RetentionPeriod && metricsCurrentTTLValues.metrics_move_ttl_duration_hrs ===
currentTTLValues.traces_ttl_duration_hrs === tracesTotalRetentionPeriod && metricsS3RetentionPeriod
currentTTLValues.traces_move_ttl_duration_hrs === tracesS3RetentionPeriod )
) { isMetricsSaveDisabled = true;
isDisabled = true;
} if (
return [isDisabled, errorText]; tracesCurrentTTLValues.traces_ttl_duration_hrs ===
tracesTotalRetentionPeriod &&
tracesCurrentTTLValues.traces_move_ttl_duration_hrs ===
tracesS3RetentionPeriod
)
isTracesSaveDisabled = true;
return [isMetricsSaveDisabled, isTracesSaveDisabled, errorText];
}, [ }, [
currentTTLValues, metricsCurrentTTLValues.metrics_move_ttl_duration_hrs,
metricsCurrentTTLValues?.metrics_ttl_duration_hrs,
metricsS3RetentionPeriod, metricsS3RetentionPeriod,
metricsTotalRetentionPeriod, metricsTotalRetentionPeriod,
s3Enabled, s3Enabled,
t, t,
tracesCurrentTTLValues.traces_move_ttl_duration_hrs,
tracesCurrentTTLValues.traces_ttl_duration_hrs,
tracesS3RetentionPeriod, tracesS3RetentionPeriod,
tracesTotalRetentionPeriod, tracesTotalRetentionPeriod,
]); ]);
// eslint-disable-next-line sonarjs/cognitive-complexity
const onOkHandler = async (type: TTTLType): Promise<void> => {
try {
onPostApiLoadingHandler(type);
const setTTLResponse = await setRetentionApi({
type,
totalDuration: `${
(type === 'metrics'
? metricsTotalRetentionPeriod
: tracesTotalRetentionPeriod) || -1
}h`,
coldStorage: s3Enabled ? 's3' : null,
toColdDuration: `${
(type === 'metrics'
? metricsS3RetentionPeriod
: tracesS3RetentionPeriod) || -1
}h`,
});
let hasSetTTLFailed = false;
if (setTTLResponse.statusCode === 409) {
hasSetTTLFailed = true;
notification.error({
message: 'Error',
description: t('retention_request_race_condition'),
placement: 'topRight',
});
}
if (type === 'metrics') {
metricsTtlValuesRefetch();
if (!hasSetTTLFailed)
// Updates the currentTTL Values in order to avoid pushing the same values.
setMetricsCurrentTTLValues({
metrics_ttl_duration_hrs: metricsTotalRetentionPeriod || -1,
metrics_move_ttl_duration_hrs: metricsS3RetentionPeriod || -1,
status: '',
});
} else if (type === 'traces') {
tracesTtlValuesRefetch();
if (!hasSetTTLFailed)
// Updates the currentTTL Values in order to avoid pushing the same values.
setTracesCurrentTTLValues({
traces_ttl_duration_hrs: tracesTotalRetentionPeriod || -1,
traces_move_ttl_duration_hrs: tracesS3RetentionPeriod || -1,
status: '',
});
}
} catch (error) {
notification.error({
message: 'Error',
description: t('retention_failed_message'),
placement: 'topRight',
});
}
onPostApiLoadingHandler(type);
onModalToggleHandler(type);
};
const renderConfig = [
{
name: 'Metrics',
retentionFields: [
{
name: t('total_retention_period'),
value: metricsTotalRetentionPeriod,
setValue: setMetricsTotalRetentionPeriod,
},
{
name: t('move_to_s3'),
value: metricsS3RetentionPeriod,
setValue: setMetricsS3RetentionPeriod,
hide: !s3Enabled,
},
],
save: {
modal: modalMetrics,
modalOpen: (): void => onClickSaveHandler('metrics'),
apiLoading: postApiLoadingMetrics,
saveButtonText:
metricsTtlValuesPayload.status === 'pending' ? (
<span>
<Spin spinning size="small" indicator={<LoadingOutlined spin />} />{' '}
{t('retention_save_button.pending', { name: 'metrics' })}
</span>
) : (
<span>{t('retention_save_button.success')}</span>
),
isDisabled:
metricsTtlValuesPayload.status === 'pending' || isMetricsSaveDisabled,
},
statusComponent: (
<StatusMessage
total_retention={metricsTtlValuesPayload.expected_metrics_ttl_duration_hrs}
status={metricsTtlValuesPayload.status}
s3_retention={
metricsTtlValuesPayload.expected_metrics_move_ttl_duration_hrs
}
/>
),
},
{
name: 'Traces',
retentionFields: [
{
name: t('total_retention_period'),
value: tracesTotalRetentionPeriod,
setValue: setTracesTotalRetentionPeriod,
},
{
name: t('move_to_s3'),
value: tracesS3RetentionPeriod,
setValue: setTracesS3RetentionPeriod,
hide: !s3Enabled,
},
],
save: {
modal: modalTraces,
modalOpen: (): void => onClickSaveHandler('traces'),
apiLoading: postApiLoadingTraces,
saveButtonText:
tracesTtlValuesPayload.status === 'pending' ? (
<span>
<Spin spinning size="small" indicator={<LoadingOutlined spin />} />{' '}
{t('retention_save_button.pending', { name: 'traces' })}
</span>
) : (
<span>{t('retention_save_button.success')}</span>
),
isDisabled:
tracesTtlValuesPayload.status === 'pending' || isTracesSaveDisabled,
},
statusComponent: (
<StatusMessage
total_retention={tracesTtlValuesPayload.expected_traces_ttl_duration_hrs}
status={tracesTtlValuesPayload.status}
s3_retention={tracesTtlValuesPayload.expected_traces_move_ttl_duration_hrs}
/>
),
},
].map((category, idx, renderArr): JSX.Element | null => {
if (
Array.isArray(category.retentionFields) &&
category.retentionFields.length > 0
) {
return (
<React.Fragment key={category.name}>
<Col xs={22} xl={11} key={category.name}>
<Typography.Title level={3}>{category.name}</Typography.Title>
{category.retentionFields.map((retentionField) => (
<Retention
key={retentionField.name}
text={retentionField.name}
retentionValue={retentionField.value}
setRetentionValue={retentionField.setValue}
hide={!!retentionField.hide}
/>
))}
<ActionItemsContainer>
<Button
type="primary"
onClick={category.save.modalOpen}
disabled={category.save.isDisabled}
>
{category.save.saveButtonText}
</Button>
{category.statusComponent}
</ActionItemsContainer>
<Modal
title={t('retention_confirmation')}
focusTriggerAfterClose
forceRender
destroyOnClose
closable
onCancel={(): void =>
onModalToggleHandler(category.name.toLowerCase() as TTTLType)
}
onOk={(): Promise<void> =>
onOkHandler(category.name.toLowerCase() as TTTLType)
}
centered
visible={category.save.modal}
confirmLoading={category.save.apiLoading}
>
<Typography>
{t('retention_confirmation_description', {
name: category.name.toLowerCase(),
})}
</Typography>
</Modal>
</Col>
{idx < renderArr.length && (
<Col xs={0} xl={1} style={{ textAlign: 'center' }}>
<Divider type="vertical" dashed style={{ height: '100%' }} />
</Col>
)}
</React.Fragment>
);
}
return null;
});
return ( return (
<Col xs={24} md={22} xl={20} xxl={18} style={{ margin: 'auto' }}> <Col xs={24} md={22} xl={20} xxl={18} style={{ margin: 'auto' }}>
{Element} {Element}
@ -306,34 +452,20 @@ function GeneralSettings({
</ErrorTextContainer> </ErrorTextContainer>
<Row justify="space-around">{renderConfig}</Row> <Row justify="space-around">{renderConfig}</Row>
<Modal
title={t('settings.retention_confirmation')}
focusTriggerAfterClose
forceRender
destroyOnClose
closable
onCancel={onModalToggleHandler}
onOk={onOkHandler}
centered
visible={modal}
confirmLoading={postApiLoading}
>
<Typography>{t('settings.retention_confirmation_description')}</Typography>
</Modal>
<ButtonContainer>
<Button onClick={onClickSaveHandler} disabled={isDisabled} type="primary">
Save
</Button>
</ButtonContainer>
</Col> </Col>
); );
} }
interface GeneralSettingsProps { interface GeneralSettingsProps {
ttlValuesPayload: GetRetentionPayload;
getAvailableDiskPayload: GetDisksPayload; getAvailableDiskPayload: GetDisksPayload;
metricsTtlValuesPayload: GetRetentionPeriodMetricsPayload;
tracesTtlValuesPayload: GetRetentionPeriodTracesPayload;
metricsTtlValuesRefetch: UseQueryResult<
ErrorResponse | SuccessResponse<GetRetentionPeriodMetricsPayload>
>['refetch'];
tracesTtlValuesRefetch: UseQueryResult<
ErrorResponse | SuccessResponse<GetRetentionPeriodTracesPayload>
>['refetch'];
} }
export default GeneralSettings; export default GeneralSettings;

View File

@ -0,0 +1,69 @@
import { green, orange, volcano } from '@ant-design/colors';
import { InfoCircleOutlined } from '@ant-design/icons';
import { Card, Col, Row } from 'antd';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { TStatus } from 'types/api/settings/getRetention';
import { convertHoursValueToRelevantUnitString } from './utils';
function StatusMessage({
total_retention,
s3_retention,
status,
}: StatusMessageProps): JSX.Element | null {
const { t } = useTranslation(['generalSettings']);
const messageColor = useMemo((): string => {
if (status === 'success') return green[6];
if (status === 'pending') return orange[6];
if (status === 'failed') return volcano[6];
return 'inherit';
}, [status]);
if (!status) {
return null;
}
const s3Part =
s3_retention && s3_retention !== -1
? t('status_message.s3_part', {
s3_retention: convertHoursValueToRelevantUnitString(s3_retention),
})
: '';
const statusMessage =
total_retention && total_retention !== -1
? t(`status_message.${status}`, {
total_retention: convertHoursValueToRelevantUnitString(total_retention),
s3_part: s3Part,
})
: null;
return statusMessage ? (
<Card
style={{
width: '100%',
}}
>
<Row style={{ gap: '1rem', alignItems: 'center', justifyContent: 'center' }}>
<Col xs={1}>
<InfoCircleOutlined style={{ fontSize: '1.5rem' }} />
</Col>
<Col
xs={22}
style={{
color: messageColor,
}}
>
{statusMessage}
</Col>
</Row>
</Card>
) : null;
}
interface StatusMessageProps {
status: TStatus;
total_retention: number | undefined;
s3_retention: number | undefined;
}
export default StatusMessage;

View File

@ -5,15 +5,33 @@ import Spinner from 'components/Spinner';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useQueries } from 'react-query'; import { useQueries } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { TTTLType } from 'types/api/settings/common';
import { PayloadProps as GetRetentionPeriodAPIPayloadProps } from 'types/api/settings/getRetention';
import GeneralSettingsContainer from './GeneralSettings'; import GeneralSettingsContainer from './GeneralSettings';
type TRetentionAPIReturn<T extends TTTLType> = Promise<
SuccessResponse<GetRetentionPeriodAPIPayloadProps<T>> | ErrorResponse
>;
function GeneralSettings(): JSX.Element { function GeneralSettings(): JSX.Element {
const { t } = useTranslation('common'); const { t } = useTranslation('common');
const [getRetentionPeriodApiResponse, getDisksResponse] = useQueries([
const [
getRetentionPeriodMetricsApiResponse,
getRetentionPeriodTracesApiResponse,
getDisksResponse,
] = useQueries([
{ {
queryFn: getRetentionPeriodApi, queryFn: (): TRetentionAPIReturn<'metrics'> =>
queryKey: 'getRetentionPeriodApi', getRetentionPeriodApi('metrics'),
queryKey: 'getRetentionPeriodApiMetrics',
},
{
queryFn: (): TRetentionAPIReturn<'traces'> =>
getRetentionPeriodApi('traces'),
queryKey: 'getRetentionPeriodApiTraces',
}, },
{ {
queryFn: getDisks, queryFn: getDisks,
@ -21,21 +39,38 @@ function GeneralSettings(): JSX.Element {
}, },
]); ]);
if (getRetentionPeriodApiResponse.isError || getDisksResponse.isError) { // Error State - When RetentionPeriodMetricsApi or getDiskApi gets errored out.
if (getRetentionPeriodMetricsApiResponse.isError || getDisksResponse.isError) {
return ( return (
<Typography> <Typography>
{getRetentionPeriodApiResponse.data?.error || {getRetentionPeriodMetricsApiResponse.data?.error ||
getDisksResponse.data?.error || getDisksResponse.data?.error ||
t('something_went_wrong')} t('something_went_wrong')}
</Typography> </Typography>
); );
} }
// Error State - When RetentionPeriodTracesApi or getDiskApi gets errored out.
if (getRetentionPeriodTracesApiResponse.isError || getDisksResponse.isError) {
return (
<Typography>
{getRetentionPeriodTracesApiResponse.data?.error ||
getDisksResponse.data?.error ||
t('something_went_wrong')}
</Typography>
);
}
// Loading State - When Metrics, Traces and Disk API are in progress and the promise has not been resolved/reject.
if ( if (
getRetentionPeriodApiResponse.isLoading || getRetentionPeriodMetricsApiResponse.isLoading ||
getDisksResponse.isLoading || getDisksResponse.isLoading ||
!getDisksResponse.data?.payload || !getDisksResponse.data?.payload ||
!getRetentionPeriodApiResponse.data?.payload !getRetentionPeriodMetricsApiResponse.data?.payload ||
getRetentionPeriodTracesApiResponse.isLoading ||
getDisksResponse.isLoading ||
!getDisksResponse.data?.payload ||
!getRetentionPeriodTracesApiResponse.data?.payload
) { ) {
return <Spinner tip="Loading.." height="70vh" />; return <Spinner tip="Loading.." height="70vh" />;
} }
@ -44,7 +79,10 @@ function GeneralSettings(): JSX.Element {
<GeneralSettingsContainer <GeneralSettingsContainer
{...{ {...{
getAvailableDiskPayload: getDisksResponse.data?.payload, getAvailableDiskPayload: getDisksResponse.data?.payload,
ttlValuesPayload: getRetentionPeriodApiResponse.data?.payload, metricsTtlValuesPayload: getRetentionPeriodMetricsApiResponse.data?.payload,
metricsTtlValuesRefetch: getRetentionPeriodMetricsApiResponse.refetch,
tracesTtlValuesPayload: getRetentionPeriodTracesApiResponse.data?.payload,
tracesTtlValuesRefetch: getRetentionPeriodTracesApiResponse.refetch,
}} }}
/> />
); );

View File

@ -90,3 +90,11 @@ export const RetentionFieldLabel = styled(TypographyComponent)`
export const RetentionFieldInputContainer = styled.div` export const RetentionFieldInputContainer = styled.div`
display: inline-flex; display: inline-flex;
`; `;
export const ActionItemsContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
margin-top: 10%;
`;

View File

@ -23,9 +23,13 @@ export const TimeUnits: ITimeUnit[] = [
}, },
]; ];
interface ITimeUnitConversion {
value: number;
timeUnitValue: SettingPeriod;
}
export const convertHoursValueToRelevantUnit = ( export const convertHoursValueToRelevantUnit = (
value: number, value: number,
): { value: number; timeUnitValue: SettingPeriod } => { ): ITimeUnitConversion => {
if (value) if (value)
for (let idx = TimeUnits.length - 1; idx >= 0; idx -= 1) { for (let idx = TimeUnits.length - 1; idx >= 0; idx -= 1) {
const timeUnit = TimeUnits[idx]; const timeUnit = TimeUnits[idx];
@ -40,3 +44,13 @@ export const convertHoursValueToRelevantUnit = (
} }
return { value, timeUnitValue: TimeUnits[0].value }; return { value, timeUnitValue: TimeUnits[0].value };
}; };
export const convertHoursValueToRelevantUnitString = (
value: number,
): string => {
if (!value) return '';
const convertedTimeUnit = convertHoursValueToRelevantUnit(value);
return `${convertedTimeUnit.value} ${convertedTimeUnit.timeUnitValue}${
convertedTimeUnit.value >= 2 ? 's' : ''
}`;
};

View File

@ -0,0 +1 @@
export type TTTLType = 'metrics' | 'traces';

View File

@ -1,6 +1,25 @@
export interface PayloadProps { import { TTTLType } from './common';
export type TStatus = '' | 'pending' | 'failed' | 'success';
export interface PayloadPropsMetrics {
metrics_ttl_duration_hrs: number; metrics_ttl_duration_hrs: number;
metrics_move_ttl_duration_hrs?: number; metrics_move_ttl_duration_hrs?: number;
status: TStatus;
expected_metrics_move_ttl_duration_hrs?: number;
expected_metrics_ttl_duration_hrs?: number;
}
export interface PayloadPropsTraces {
traces_ttl_duration_hrs: number; traces_ttl_duration_hrs: number;
traces_move_ttl_duration_hrs?: number; traces_move_ttl_duration_hrs?: number;
status: TStatus;
expected_traces_move_ttl_duration_hrs?: number;
expected_traces_ttl_duration_hrs?: number;
} }
export type Props = TTTLType;
export type PayloadProps<T> = T extends 'metrics'
? PayloadPropsMetrics
: T extends 'traces'
? PayloadPropsTraces
: never;

View File

@ -1,5 +1,7 @@
import { TTTLType } from './common';
export interface Props { export interface Props {
type: 'metrics' | 'traces'; type: TTTLType;
totalDuration: string; totalDuration: string;
coldStorage?: 's3' | null; coldStorage?: 's3' | null;
toColdDuration?: string; toColdDuration?: string;

View File

@ -10,6 +10,8 @@ export type Unauthorized = 401;
export type NotFound = 404; export type NotFound = 404;
export type Conflict = 409;
export type ServerError = 500; export type ServerError = 500;
export type SuccessStatusCode = Created | Success; export type SuccessStatusCode = Created | Success;
@ -20,6 +22,7 @@ export type ErrorStatusCode =
| Unauthorized | Unauthorized
| NotFound | NotFound
| ServerError | ServerError
| BadRequest; | BadRequest
| Conflict;
export type StatusCode = SuccessStatusCode | ErrorStatusCode; export type StatusCode = SuccessStatusCode | ErrorStatusCode;