mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-25 17:52:23 +00:00
Compare commits
2 Commits
fix/remove
...
feat/datet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f040b0400 | ||
|
|
b73d5b2ff2 |
@@ -2,8 +2,50 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
|
.date-time-picker-container {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.date-time-picker-input {
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px 6px 6px 8px;
|
||||||
|
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid var(--Slate-400, #1d212d);
|
||||||
|
background: var(--Ink-300, #16181d);
|
||||||
|
|
||||||
|
&.custom-time {
|
||||||
|
.ant-input {
|
||||||
|
min-width: 280px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-time-picker-content {
|
||||||
|
min-width: 580px;
|
||||||
|
max-width: 580px;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 36px; // 32px + 4px
|
||||||
|
right: 0;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 1000;
|
||||||
|
|
||||||
|
border-radius: 4px !important;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2) !important;
|
||||||
|
padding: 0px !important;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: linear-gradient(
|
||||||
|
139deg,
|
||||||
|
rgba(18, 19, 23, 0.8) 0%,
|
||||||
|
rgba(18, 19, 23, 0.9) 98.68%
|
||||||
|
) !important;
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.timeSelection-input {
|
.timeSelection-input {
|
||||||
&:hover {
|
&:hover:not(.ant-input-affix-wrapper-status-error) {
|
||||||
border-color: #1d212d !important;
|
border-color: #1d212d !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,11 +91,16 @@
|
|||||||
padding-left: 0px !important;
|
padding-left: 0px !important;
|
||||||
|
|
||||||
&.custom-time {
|
&.custom-time {
|
||||||
input:not(:focus) {
|
input {
|
||||||
min-width: 280px;
|
min-width: 280px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-input {
|
||||||
|
background: var(--Ink-300, #16181d);
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
|
||||||
input::placeholder {
|
input::placeholder {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
@@ -120,6 +167,11 @@
|
|||||||
color: var(---bg-ink-300);
|
color: var(---bg-ink-300);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-input {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
input:focus::placeholder {
|
input:focus::placeholder {
|
||||||
color: rgba($color: #000000, $alpha: 0.4);
|
color: rgba($color: #000000, $alpha: 0.4);
|
||||||
}
|
}
|
||||||
@@ -239,7 +291,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.custom-time-picker {
|
.custom-time-picker {
|
||||||
.timeSelection-input {
|
.timeSelection-input:not(.ant-input-affix-wrapper-status-error) {
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: var(--bg-vanilla-300) !important;
|
border-color: var(--bg-vanilla-300) !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||||
import './CustomTimePicker.styles.scss';
|
import './CustomTimePicker.styles.scss';
|
||||||
|
|
||||||
import { Input, Popover, Tooltip, Typography } from 'antd';
|
import type { InputRef } from 'antd';
|
||||||
|
import { Input, Tooltip, Typography } from 'antd';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||||
@@ -15,7 +16,6 @@ import {
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { isValidTimeFormat } from 'lib/getMinMax';
|
import { isValidTimeFormat } from 'lib/getMinMax';
|
||||||
import { defaultTo, isFunction, noop } from 'lodash-es';
|
import { defaultTo, isFunction, noop } from 'lodash-es';
|
||||||
import debounce from 'lodash-es/debounce';
|
|
||||||
import { CheckCircle, ChevronDown, Clock } from 'lucide-react';
|
import { CheckCircle, ChevronDown, Clock } from 'lucide-react';
|
||||||
import { useTimezone } from 'providers/Timezone';
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import {
|
import {
|
||||||
@@ -25,13 +25,13 @@ import {
|
|||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
import { popupContainer } from 'utils/selectPopupContainer';
|
|
||||||
|
|
||||||
import CustomTimePickerPopoverContent from './CustomTimePickerPopoverContent';
|
import CustomTimePickerPopoverContent from './CustomTimePickerPopoverContent';
|
||||||
|
|
||||||
@@ -64,6 +64,150 @@ interface CustomTimePickerProps {
|
|||||||
onExitLiveLogs?: () => void;
|
onExitLiveLogs?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formatSelectedTimeValue = (selectedTime: string): string => {
|
||||||
|
console.log('selectedTime', selectedTime);
|
||||||
|
|
||||||
|
// format valid time format to 12-hour format
|
||||||
|
// 1m -> Last 1 minute
|
||||||
|
// 1h -> Last 1 hour
|
||||||
|
// 1d -> Last 1 day
|
||||||
|
// 1w -> Last 1 week
|
||||||
|
// 30m -> Last 30 minutes
|
||||||
|
// 6h -> Last 6 hours
|
||||||
|
// 3d -> Last 3 days
|
||||||
|
// 1w -> Last 1 week
|
||||||
|
// 1month -> Last 1 month
|
||||||
|
// 2months -> Last 2 months
|
||||||
|
|
||||||
|
// parse the string to generate the label
|
||||||
|
const regex = /^(\d+)([mhdw])$/;
|
||||||
|
const match = regex.exec(selectedTime);
|
||||||
|
if (match) {
|
||||||
|
const value = match[1];
|
||||||
|
const unit = match[2];
|
||||||
|
|
||||||
|
const intValue = parseInt(value, 10);
|
||||||
|
|
||||||
|
switch (unit) {
|
||||||
|
case 'm':
|
||||||
|
return `Last ${value} minutes`;
|
||||||
|
case 'h':
|
||||||
|
return `Last ${value} hour${intValue > 1 ? 's' : ''}`;
|
||||||
|
case 'd':
|
||||||
|
return `Last ${value} day${intValue > 1 ? 's' : ''}`;
|
||||||
|
case 'w':
|
||||||
|
return `Last ${value} week${intValue > 1 ? 's' : ''}`;
|
||||||
|
case 'month':
|
||||||
|
return `Last ${value} month${intValue > 1 ? 's' : ''}`;
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSelectedTimeRangeLabel = (
|
||||||
|
selectedTime: string,
|
||||||
|
selectedTimeValue: string,
|
||||||
|
): string => {
|
||||||
|
let selectedTimeLabel = '';
|
||||||
|
|
||||||
|
if (selectedTime === 'custom') {
|
||||||
|
// TODO(shaheer): if the user preference is 12 hour format, then convert the date range string to 12-hour format (pick this up while working on 12/24 hour preference feature)
|
||||||
|
// // Convert the date range string to 12-hour format
|
||||||
|
// const dates = selectedTimeValue.split(' - ');
|
||||||
|
// if (dates.length === 2) {
|
||||||
|
// const startDate = dayjs(dates[0], DATE_TIME_FORMATS.UK_DATETIME);
|
||||||
|
// const endDate = dayjs(dates[1], DATE_TIME_FORMATS.UK_DATETIME);
|
||||||
|
|
||||||
|
// return `${startDate.format(DATE_TIME_FORMATS.UK_DATETIME)} - ${endDate.format(
|
||||||
|
// DATE_TIME_FORMATS.UK_DATETIME,
|
||||||
|
// )}`;
|
||||||
|
// }
|
||||||
|
return selectedTimeValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let index = 0; index < Options.length; index++) {
|
||||||
|
if (Options[index].value === selectedTime) {
|
||||||
|
selectedTimeLabel = Options[index].label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (
|
||||||
|
let index = 0;
|
||||||
|
index < RelativeDurationSuggestionOptions.length;
|
||||||
|
index++
|
||||||
|
) {
|
||||||
|
if (RelativeDurationSuggestionOptions[index].value === selectedTime) {
|
||||||
|
selectedTimeLabel = RelativeDurationSuggestionOptions[index].label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let index = 0; index < FixedDurationSuggestionOptions.length; index++) {
|
||||||
|
if (FixedDurationSuggestionOptions[index].value === selectedTime) {
|
||||||
|
selectedTimeLabel = FixedDurationSuggestionOptions[index].label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isValidTimeFormat(selectedTime)) {
|
||||||
|
selectedTimeLabel = formatSelectedTimeValue(selectedTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedTimeLabel;
|
||||||
|
};
|
||||||
|
|
||||||
|
// const getFormattedSelectedTimeValue = (
|
||||||
|
// selectedTime: string,
|
||||||
|
// selectedTimeValue: string,
|
||||||
|
// ): string => {
|
||||||
|
// console.log('selectedTime', selectedTime);
|
||||||
|
// console.log('selectedTimeValue', selectedTimeValue);
|
||||||
|
|
||||||
|
// if (selectedTime === 'custom') {
|
||||||
|
// // TODO(shaheer): if the user preference is 12 hour format, then convert the date range string to 12-hour format (pick this up while working on 12/24 hour preference feature)
|
||||||
|
// // // Convert the date range string to 12-hour format
|
||||||
|
// // const dates = selectedTimeValue.split(' - ');
|
||||||
|
// // if (dates.length === 2) {
|
||||||
|
// // const startDate = dayjs(dates[0], DATE_TIME_FORMATS.UK_DATETIME);
|
||||||
|
// // const endDate = dayjs(dates[1], DATE_TIME_FORMATS.UK_DATETIME);
|
||||||
|
|
||||||
|
// // return `${startDate.format(DATE_TIME_FORMATS.UK_DATETIME)} - ${endDate.format(
|
||||||
|
// // DATE_TIME_FORMATS.UK_DATETIME,
|
||||||
|
// // )}`;
|
||||||
|
// // }
|
||||||
|
// return selectedTimeValue;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// for (let index = 0; index < Options.length; index++) {
|
||||||
|
// if (Options[index].value === selectedTime) {
|
||||||
|
// return Options[index].label;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// for (
|
||||||
|
// let index = 0;
|
||||||
|
// index < RelativeDurationSuggestionOptions.length;
|
||||||
|
// index++
|
||||||
|
// ) {
|
||||||
|
// if (RelativeDurationSuggestionOptions[index].value === selectedTime) {
|
||||||
|
// return RelativeDurationSuggestionOptions[index].label;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// for (let index = 0; index < FixedDurationSuggestionOptions.length; index++) {
|
||||||
|
// if (FixedDurationSuggestionOptions[index].value === selectedTime) {
|
||||||
|
// return FixedDurationSuggestionOptions[index].label;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (isValidTimeFormat(selectedTime)) {
|
||||||
|
// return selectedTime;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return '';
|
||||||
|
// };
|
||||||
|
|
||||||
function CustomTimePicker({
|
function CustomTimePicker({
|
||||||
onSelect,
|
onSelect,
|
||||||
onError,
|
onError,
|
||||||
@@ -91,18 +235,34 @@ function CustomTimePicker({
|
|||||||
(state) => state.globalTime,
|
(state) => state.globalTime,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [inputValue, setInputValue] = useState('');
|
const inputRef = useRef<InputRef | null>(null);
|
||||||
|
|
||||||
|
const [value, setValue] = useState<string>('');
|
||||||
|
|
||||||
const [inputStatus, setInputStatus] = useState<'' | 'error' | 'success'>('');
|
const [inputStatus, setInputStatus] = useState<'' | 'error' | 'success'>('');
|
||||||
const [inputErrorMessage, setInputErrorMessage] = useState<string | null>(
|
const [inputErrorMessage, setInputErrorMessage] = useState<string | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
|
const [showDateTimeOptions, setShowDateTimeOptions] = useState(open);
|
||||||
|
|
||||||
const [isInputFocused, setIsInputFocused] = useState(false);
|
const [isInputFocused, setIsInputFocused] = useState(false);
|
||||||
|
|
||||||
const [activeView, setActiveView] = useState<ViewType>(DEFAULT_VIEW);
|
const [activeView, setActiveView] = useState<ViewType>(DEFAULT_VIEW);
|
||||||
|
|
||||||
const { timezone, browserTimezone } = useTimezone();
|
const { timezone, browserTimezone } = useTimezone();
|
||||||
|
|
||||||
const activeTimezoneOffset = timezone.offset;
|
const activeTimezoneOffset = timezone.offset;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setShowDateTimeOptions(open);
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setValue(getSelectedTimeRangeLabel(selectedTime, selectedValue));
|
||||||
|
}, [selectedTime, selectedValue]);
|
||||||
|
|
||||||
const isTimezoneOverridden = useMemo(
|
const isTimezoneOverridden = useMemo(
|
||||||
() => timezone.offset !== browserTimezone.offset,
|
() => timezone.offset !== browserTimezone.offset,
|
||||||
[timezone, browserTimezone],
|
[timezone, browserTimezone],
|
||||||
@@ -120,60 +280,14 @@ function CustomTimePicker({
|
|||||||
|
|
||||||
const [isOpenedFromFooter, setIsOpenedFromFooter] = useState(false);
|
const [isOpenedFromFooter, setIsOpenedFromFooter] = useState(false);
|
||||||
|
|
||||||
const getSelectedTimeRangeLabel = (
|
|
||||||
selectedTime: string,
|
|
||||||
selectedTimeValue: string,
|
|
||||||
): string => {
|
|
||||||
if (selectedTime === 'custom') {
|
|
||||||
// TODO(shaheer): if the user preference is 12 hour format, then convert the date range string to 12-hour format (pick this up while working on 12/24 hour preference feature)
|
|
||||||
// // Convert the date range string to 12-hour format
|
|
||||||
// const dates = selectedTimeValue.split(' - ');
|
|
||||||
// if (dates.length === 2) {
|
|
||||||
// const startDate = dayjs(dates[0], DATE_TIME_FORMATS.UK_DATETIME);
|
|
||||||
// const endDate = dayjs(dates[1], DATE_TIME_FORMATS.UK_DATETIME);
|
|
||||||
|
|
||||||
// return `${startDate.format(DATE_TIME_FORMATS.UK_DATETIME)} - ${endDate.format(
|
|
||||||
// DATE_TIME_FORMATS.UK_DATETIME,
|
|
||||||
// )}`;
|
|
||||||
// }
|
|
||||||
return selectedTimeValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let index = 0; index < Options.length; index++) {
|
|
||||||
if (Options[index].value === selectedTime) {
|
|
||||||
return Options[index].label;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (
|
|
||||||
let index = 0;
|
|
||||||
index < RelativeDurationSuggestionOptions.length;
|
|
||||||
index++
|
|
||||||
) {
|
|
||||||
if (RelativeDurationSuggestionOptions[index].value === selectedTime) {
|
|
||||||
return RelativeDurationSuggestionOptions[index].label;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let index = 0; index < FixedDurationSuggestionOptions.length; index++) {
|
|
||||||
if (FixedDurationSuggestionOptions[index].value === selectedTime) {
|
|
||||||
return FixedDurationSuggestionOptions[index].label;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isValidTimeFormat(selectedTime)) {
|
|
||||||
return selectedTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (showLiveLogs) {
|
if (showLiveLogs) {
|
||||||
setSelectedTimePlaceholderValue('Live');
|
setSelectedTimePlaceholderValue('Live');
|
||||||
} else {
|
} else {
|
||||||
const value = getSelectedTimeRangeLabel(selectedTime, selectedValue);
|
const value = getSelectedTimeRangeLabel(selectedTime, selectedValue);
|
||||||
setSelectedTimePlaceholderValue(value);
|
setSelectedTimePlaceholderValue(value);
|
||||||
|
|
||||||
|
setValue(value);
|
||||||
}
|
}
|
||||||
}, [selectedTime, selectedValue, showLiveLogs]);
|
}, [selectedTime, selectedValue, showLiveLogs]);
|
||||||
|
|
||||||
@@ -181,25 +295,52 @@ function CustomTimePicker({
|
|||||||
setOpen(false);
|
setOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenChange = (newOpen: boolean): void => {
|
const handleCustomDateChange = (inputValue: string): void => {
|
||||||
setOpen(newOpen);
|
const dates = inputValue.split(' - ');
|
||||||
if (!newOpen) {
|
|
||||||
setCustomDTPickerVisible?.(false);
|
const startDate = dayjs(dates[0], DATE_TIME_FORMATS.UK_DATETIME_SECONDS);
|
||||||
setActiveView('datetime');
|
const endDate = dayjs(dates[1], DATE_TIME_FORMATS.UK_DATETIME_SECONDS);
|
||||||
|
|
||||||
|
if (!startDate.isValid() || !endDate.isValid()) {
|
||||||
|
setInputStatus('error');
|
||||||
|
onError(true);
|
||||||
|
setInputErrorMessage('Please enter valid date range');
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (startDate.isAfter(endDate)) {
|
||||||
|
setInputStatus('error');
|
||||||
|
onError(true);
|
||||||
|
setInputErrorMessage('Start date should be before end date');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onCustomDateHandler?.([startDate, endDate]);
|
||||||
|
|
||||||
|
setInputStatus('success');
|
||||||
|
onError(false);
|
||||||
|
setInputErrorMessage(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const debouncedHandleInputChange = debounce((inputValue): void => {
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
|
const handleDateTimeChange = (inputValue: string): void => {
|
||||||
|
if (!inputValue || inputValue === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const isValidFormat = /^(\d+)([mhdw])$/.test(inputValue);
|
const isValidFormat = /^(\d+)([mhdw])$/.test(inputValue);
|
||||||
if (isValidFormat) {
|
|
||||||
|
if (inputValue && isValidFormat) {
|
||||||
setInputStatus('success');
|
setInputStatus('success');
|
||||||
onError(false);
|
onError(false);
|
||||||
setInputErrorMessage(null);
|
setInputErrorMessage(null);
|
||||||
|
|
||||||
const match = inputValue.match(/^(\d+)([mhdw])$/);
|
const match = inputValue.match(/^(\d+)([mhdw])$/);
|
||||||
|
|
||||||
const value = parseInt(match[1], 10);
|
const value = match ? parseInt(match[1], 10) : 0;
|
||||||
const unit = match[2];
|
const unit = match ? match[2] : null;
|
||||||
|
|
||||||
const currentTime = dayjs();
|
const currentTime = dayjs();
|
||||||
const maxAllowedMinTime = currentTime.subtract(
|
const maxAllowedMinTime = currentTime.subtract(
|
||||||
@@ -239,6 +380,8 @@ function CustomTimePicker({
|
|||||||
timeStr: inputValue,
|
timeStr: inputValue,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else if (selectedTime === 'custom') {
|
||||||
|
handleCustomDateChange(inputValue);
|
||||||
} else {
|
} else {
|
||||||
setInputStatus('error');
|
setInputStatus('error');
|
||||||
onError(true);
|
onError(true);
|
||||||
@@ -247,21 +390,6 @@ function CustomTimePicker({
|
|||||||
onCustomTimeStatusUpdate(false);
|
onCustomTimeStatusUpdate(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 300);
|
|
||||||
|
|
||||||
const handleInputChange = (event: ChangeEvent<HTMLInputElement>): void => {
|
|
||||||
const inputValue = event.target.value;
|
|
||||||
|
|
||||||
if (inputValue.length > 0) {
|
|
||||||
setOpen(false);
|
|
||||||
} else {
|
|
||||||
setOpen(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
setInputValue(inputValue);
|
|
||||||
|
|
||||||
// Call the debounced function with the input value
|
|
||||||
debouncedHandleInputChange(inputValue);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelect = (label: string, value: string): void => {
|
const handleSelect = (label: string, value: string): void => {
|
||||||
@@ -273,19 +401,30 @@ function CustomTimePicker({
|
|||||||
onSelect(value);
|
onSelect(value);
|
||||||
setSelectedTimePlaceholderValue(label);
|
setSelectedTimePlaceholderValue(label);
|
||||||
setInputStatus('');
|
setInputStatus('');
|
||||||
|
|
||||||
|
inputRef.current?.input?.blur();
|
||||||
|
setIsInputFocused(false);
|
||||||
onError(false);
|
onError(false);
|
||||||
setInputErrorMessage(null);
|
setInputErrorMessage(null);
|
||||||
setInputValue('');
|
|
||||||
if (value !== 'custom') {
|
if (value !== 'custom') {
|
||||||
hide();
|
hide();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRecentlyUsedTimeRangeClick = (): void => {
|
||||||
|
setInputStatus('');
|
||||||
|
inputRef.current?.input?.blur();
|
||||||
|
};
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<div className="time-selection-dropdown-content">
|
<div className="time-selection-dropdown-content">
|
||||||
<div className="time-options-container">
|
<div className="time-options-container">
|
||||||
{items?.map(({ value, label }) => (
|
{items?.map(({ value, label }) => (
|
||||||
<div
|
<div
|
||||||
|
onMouseDown={(e): void => {
|
||||||
|
// Prevent blur when clicking on options
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
handleSelect(label, value);
|
handleSelect(label, value);
|
||||||
}}
|
}}
|
||||||
@@ -304,19 +443,82 @@ function CustomTimePicker({
|
|||||||
|
|
||||||
const handleFocus = (): void => {
|
const handleFocus = (): void => {
|
||||||
setIsInputFocused(true);
|
setIsInputFocused(true);
|
||||||
setActiveView('datetime');
|
setInputStatus('');
|
||||||
|
setInputErrorMessage(null);
|
||||||
|
|
||||||
|
// Get the raw/editable format for the current selection
|
||||||
|
let editableValue = selectedTime;
|
||||||
|
|
||||||
|
// If it's a custom time, use the selectedValue (date range string)
|
||||||
|
if (selectedTime === 'custom') {
|
||||||
|
editableValue = selectedValue;
|
||||||
|
}
|
||||||
|
// If it's a predefined option, convert back to raw format
|
||||||
|
else if (selectedTime && selectedTime !== 'custom') {
|
||||||
|
// For predefined options, use the selectedTime as is (like "5m", "1h")
|
||||||
|
editableValue = selectedTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update state with the raw format for editing
|
||||||
|
setValue(editableValue);
|
||||||
|
|
||||||
|
setOpen(true);
|
||||||
|
// setCustomDTPickerVisible?.(true);
|
||||||
|
setShowDateTimeOptions(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||||
|
// Update the value state for controlled input
|
||||||
|
setValue(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEnter = (): void => {
|
||||||
|
const newVal = value;
|
||||||
|
setValue('');
|
||||||
|
|
||||||
|
setIsInputFocused(false);
|
||||||
|
if (newVal !== selectedValue) {
|
||||||
|
handleDateTimeChange(newVal);
|
||||||
|
}
|
||||||
|
if (inputRef.current?.input) {
|
||||||
|
inputRef.current.input.blur();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBlur = (): void => {
|
const handleBlur = (): void => {
|
||||||
|
if (!isInputFocused) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't close if custom date picker is visible
|
||||||
|
if (customDateTimeVisible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsInputFocused(false);
|
setIsInputFocused(false);
|
||||||
|
|
||||||
|
const newVal = value;
|
||||||
|
setValue('');
|
||||||
|
|
||||||
|
if (newVal !== selectedValue) {
|
||||||
|
handleDateTimeChange(newVal);
|
||||||
|
}
|
||||||
|
if (inputRef.current?.input) {
|
||||||
|
inputRef.current.input.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpen(false);
|
||||||
|
setCustomDTPickerVisible?.(false);
|
||||||
|
setShowDateTimeOptions(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// No need for manual DOM sync with controlled input
|
||||||
|
|
||||||
// this is required as TopNav component wraps the components and we need to clear the state on path change
|
// this is required as TopNav component wraps the components and we need to clear the state on path change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setInputStatus('');
|
setInputStatus('');
|
||||||
onError(false);
|
onError(false);
|
||||||
setInputErrorMessage(null);
|
setInputErrorMessage(null);
|
||||||
setInputValue('');
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [location.pathname]);
|
}, [location.pathname]);
|
||||||
|
|
||||||
@@ -333,7 +535,7 @@ function CustomTimePicker({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getTooltipTitle = (): string => {
|
const getTooltipTitle = (): string => {
|
||||||
if (selectedTime === 'custom' && inputValue === '' && !open) {
|
if (selectedTime === 'custom' && value === '' && !open) {
|
||||||
return `${dayjs(minTime / 1000_000)
|
return `${dayjs(minTime / 1000_000)
|
||||||
.tz(timezone.value)
|
.tz(timezone.value)
|
||||||
.format(DATE_TIME_FORMATS.DD_MMM_YYYY_HH_MM_SS)} - ${dayjs(
|
.format(DATE_TIME_FORMATS.DD_MMM_YYYY_HH_MM_SS)} - ${dayjs(
|
||||||
@@ -357,7 +559,7 @@ function CustomTimePicker({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="time-input-prefix">
|
<div className="time-input-prefix">
|
||||||
{inputValue && inputStatus === 'success' ? (
|
{value && inputStatus === 'success' ? (
|
||||||
<CheckCircle size={14} color="#51E7A8" />
|
<CheckCircle size={14} color="#51E7A8" />
|
||||||
) : (
|
) : (
|
||||||
<Tooltip title="Enter time in format (e.g., 1m, 2h, 3d, 4w)">
|
<Tooltip title="Enter time in format (e.g., 1m, 2h, 3d, 4w)">
|
||||||
@@ -371,57 +573,55 @@ function CustomTimePicker({
|
|||||||
return (
|
return (
|
||||||
<div className="custom-time-picker">
|
<div className="custom-time-picker">
|
||||||
<Tooltip title={getTooltipTitle()} placement="top">
|
<Tooltip title={getTooltipTitle()} placement="top">
|
||||||
<Popover
|
<div className="date-time-picker-container">
|
||||||
className={cx(
|
{/* <Popover
|
||||||
'timeSelection-input-container',
|
className={cx(
|
||||||
selectedTime === 'custom' && inputValue === '' ? 'custom-time' : '',
|
'timeSelection-input-container',
|
||||||
)}
|
selectedTime === 'custom' && inputValue === '' ? 'custom-time' : '',
|
||||||
placement="bottomRight"
|
)}
|
||||||
getPopupContainer={popupContainer}
|
placement="bottomRight"
|
||||||
rootClassName="date-time-root"
|
getPopupContainer={popupContainer}
|
||||||
content={
|
rootClassName="date-time-root"
|
||||||
newPopover ? (
|
content={
|
||||||
<CustomTimePickerPopoverContent
|
newPopover ? (
|
||||||
setIsOpen={setOpen}
|
<CustomTimePickerPopoverContent
|
||||||
customDateTimeVisible={defaultTo(customDateTimeVisible, false)}
|
setIsOpen={setOpen}
|
||||||
setCustomDTPickerVisible={defaultTo(setCustomDTPickerVisible, noop)}
|
customDateTimeVisible={defaultTo(customDateTimeVisible, false)}
|
||||||
onCustomDateHandler={defaultTo(onCustomDateHandler, noop)}
|
setCustomDTPickerVisible={defaultTo(setCustomDTPickerVisible, noop)}
|
||||||
onSelectHandler={handleSelect}
|
onCustomDateHandler={defaultTo(onCustomDateHandler, noop)}
|
||||||
onGoLive={defaultTo(onGoLive, noop)}
|
onSelectHandler={handleSelect}
|
||||||
onExitLiveLogs={defaultTo(onExitLiveLogs, noop)}
|
onGoLive={defaultTo(onGoLive, noop)}
|
||||||
options={items}
|
onExitLiveLogs={defaultTo(onExitLiveLogs, noop)}
|
||||||
selectedTime={selectedTime}
|
options={items}
|
||||||
activeView={activeView}
|
selectedTime={selectedTime}
|
||||||
setActiveView={setActiveView}
|
activeView={activeView}
|
||||||
setIsOpenedFromFooter={setIsOpenedFromFooter}
|
setActiveView={setActiveView}
|
||||||
isOpenedFromFooter={isOpenedFromFooter}
|
setIsOpenedFromFooter={setIsOpenedFromFooter}
|
||||||
/>
|
isOpenedFromFooter={isOpenedFromFooter}
|
||||||
) : (
|
/>
|
||||||
content
|
) : (
|
||||||
)
|
content
|
||||||
}
|
)
|
||||||
arrow={false}
|
|
||||||
trigger="click"
|
|
||||||
open={open}
|
|
||||||
onOpenChange={handleOpenChange}
|
|
||||||
style={{
|
|
||||||
padding: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
className="timeSelection-input"
|
|
||||||
type="text"
|
|
||||||
status={inputValue && inputStatus === 'error' ? 'error' : ''}
|
|
||||||
placeholder={
|
|
||||||
isInputFocused
|
|
||||||
? 'Time Format (1m or 2h or 3d or 4w)'
|
|
||||||
: selectedTimePlaceholderValue
|
|
||||||
}
|
}
|
||||||
value={inputValue}
|
arrow={false}
|
||||||
onFocus={handleFocus}
|
trigger="click"
|
||||||
onClick={handleFocus}
|
open={open}
|
||||||
|
> */}
|
||||||
|
|
||||||
|
<Input
|
||||||
|
className={cx(
|
||||||
|
'date-time-picker-input timeSelection-input',
|
||||||
|
selectedTime === 'custom' ? 'custom-time' : '',
|
||||||
|
)}
|
||||||
|
type="text"
|
||||||
|
status={value && inputStatus === 'error' ? 'error' : ''}
|
||||||
|
placeholder={selectedTimePlaceholderValue}
|
||||||
|
ref={inputRef}
|
||||||
|
value={isInputFocused ? value : ''}
|
||||||
|
onChange={handleChange}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
onChange={handleInputChange}
|
onPressEnter={handleEnter}
|
||||||
|
onClick={handleFocus}
|
||||||
data-1p-ignore
|
data-1p-ignore
|
||||||
prefix={getInputPrefix()}
|
prefix={getInputPrefix()}
|
||||||
suffix={
|
suffix={
|
||||||
@@ -431,18 +631,45 @@ function CustomTimePicker({
|
|||||||
<span>{activeTimezoneOffset}</span>
|
<span>{activeTimezoneOffset}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ChevronDown
|
<ChevronDown size={14} />
|
||||||
size={14}
|
|
||||||
className="cursor-pointer time-input-suffix-icon-badge"
|
|
||||||
onClick={(e): void => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleViewChange('datetime');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Popover>
|
|
||||||
|
{showDateTimeOptions && (
|
||||||
|
<div
|
||||||
|
className="date-time-picker-content date-time-root"
|
||||||
|
onMouseDown={(e): void => {
|
||||||
|
// Prevent blur when clicking inside the popover
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{newPopover ? (
|
||||||
|
<CustomTimePickerPopoverContent
|
||||||
|
setIsOpen={setOpen}
|
||||||
|
customDateTimeVisible={defaultTo(customDateTimeVisible, false)}
|
||||||
|
setCustomDTPickerVisible={defaultTo(setCustomDTPickerVisible, noop)}
|
||||||
|
onCustomDateHandler={defaultTo(onCustomDateHandler, noop)}
|
||||||
|
onHandleRecentlyUsedTimeRangeClick={defaultTo(
|
||||||
|
handleRecentlyUsedTimeRangeClick,
|
||||||
|
noop,
|
||||||
|
)}
|
||||||
|
onSelectHandler={handleSelect}
|
||||||
|
onGoLive={defaultTo(onGoLive, noop)}
|
||||||
|
onExitLiveLogs={defaultTo(onExitLiveLogs, noop)}
|
||||||
|
options={items}
|
||||||
|
selectedTime={selectedTime}
|
||||||
|
activeView={activeView}
|
||||||
|
setActiveView={setActiveView}
|
||||||
|
setIsOpenedFromFooter={setIsOpenedFromFooter}
|
||||||
|
isOpenedFromFooter={isOpenedFromFooter}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
content
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{inputStatus === 'error' && inputErrorMessage && (
|
{inputStatus === 'error' && inputErrorMessage && (
|
||||||
<Typography.Title level={5} className="valid-format-error">
|
<Typography.Title level={5} className="valid-format-error">
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ interface CustomTimePickerPopoverContentProps {
|
|||||||
isOpenedFromFooter: boolean;
|
isOpenedFromFooter: boolean;
|
||||||
setIsOpenedFromFooter: Dispatch<SetStateAction<boolean>>;
|
setIsOpenedFromFooter: Dispatch<SetStateAction<boolean>>;
|
||||||
onExitLiveLogs: () => void;
|
onExitLiveLogs: () => void;
|
||||||
|
onHandleRecentlyUsedTimeRangeClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RecentlyUsedDateTimeRange {
|
interface RecentlyUsedDateTimeRange {
|
||||||
@@ -72,6 +73,7 @@ function CustomTimePickerPopoverContent({
|
|||||||
isOpenedFromFooter,
|
isOpenedFromFooter,
|
||||||
setIsOpenedFromFooter,
|
setIsOpenedFromFooter,
|
||||||
onExitLiveLogs,
|
onExitLiveLogs,
|
||||||
|
onHandleRecentlyUsedTimeRangeClick,
|
||||||
}: CustomTimePickerPopoverContentProps): JSX.Element {
|
}: CustomTimePickerPopoverContentProps): JSX.Element {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
@@ -224,33 +226,37 @@ function CustomTimePickerPopoverContent({
|
|||||||
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
|
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="recently-used-container">
|
{recentlyUsedTimeRanges && recentlyUsedTimeRanges.length > 0 && (
|
||||||
<div className="time-heading">RECENTLY USED</div>
|
<div className="recently-used-container">
|
||||||
<div className="recently-used-range">
|
<div className="time-heading">RECENTLY USED</div>
|
||||||
{recentlyUsedTimeRanges.map((range: RecentlyUsedDateTimeRange) => (
|
<div className="recently-used-range">
|
||||||
<div
|
{recentlyUsedTimeRanges.map((range: RecentlyUsedDateTimeRange) => (
|
||||||
className="recently-used-range-item"
|
<div
|
||||||
role="button"
|
className="recently-used-range-item"
|
||||||
tabIndex={0}
|
role="button"
|
||||||
onKeyDown={(e): void => {
|
tabIndex={0}
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
onKeyDown={(e): void => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
handleExitLiveLogs();
|
||||||
|
onCustomDateHandler([dayjs(range.from), dayjs(range.to)]);
|
||||||
|
onHandleRecentlyUsedTimeRangeClick();
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
key={range.value}
|
||||||
|
onClick={(): void => {
|
||||||
handleExitLiveLogs();
|
handleExitLiveLogs();
|
||||||
onCustomDateHandler([dayjs(range.from), dayjs(range.to)]);
|
onCustomDateHandler([dayjs(range.from), dayjs(range.to)]);
|
||||||
|
onHandleRecentlyUsedTimeRangeClick();
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}
|
}}
|
||||||
}}
|
>
|
||||||
key={range.value}
|
{range.label}
|
||||||
onClick={(): void => {
|
</div>
|
||||||
handleExitLiveLogs();
|
))}
|
||||||
onCustomDateHandler([dayjs(range.from), dayjs(range.to)]);
|
</div>
|
||||||
setIsOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{range.label}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ $item-spacing: 8px;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.timezone-picker {
|
.timezone-picker {
|
||||||
width: 532px;
|
width: 100%;
|
||||||
color: var(--bg-vanilla-400);
|
color: var(--bg-vanilla-400);
|
||||||
font-family: $font-family;
|
font-family: $font-family;
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,13 @@ function DatePickerV2({
|
|||||||
|
|
||||||
const handleNext = (): void => {
|
const handleNext = (): void => {
|
||||||
if (selectedDateTimeFor === 'to') {
|
if (selectedDateTimeFor === 'to') {
|
||||||
|
console.log(
|
||||||
|
'handleNext selectedFromDateTime',
|
||||||
|
selectedFromDateTime,
|
||||||
|
'selectedToDateTime',
|
||||||
|
selectedToDateTime,
|
||||||
|
);
|
||||||
|
|
||||||
onCustomDateHandler([selectedFromDateTime, selectedToDateTime]);
|
onCustomDateHandler([selectedFromDateTime, selectedToDateTime]);
|
||||||
|
|
||||||
addCustomTimeRange([selectedFromDateTime, selectedToDateTime]);
|
addCustomTimeRange([selectedFromDateTime, selectedToDateTime]);
|
||||||
|
|||||||
@@ -40,13 +40,16 @@ export default function RightToolbarActions({
|
|||||||
if (showLiveLogs) return <div />;
|
if (showLiveLogs) return <div />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="right-toolbar">
|
||||||
{isLoadingQueries ? (
|
{isLoadingQueries ? (
|
||||||
<div className="loading-container">
|
<div className="query-in-progress-container">
|
||||||
<Button className="loading-btn" loading={isLoadingQueries} />
|
<Button
|
||||||
|
className="periscope-btn ghost query-in-progress-btn"
|
||||||
|
loading={isLoadingQueries}
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
icon={<X size={14} />}
|
icon={<X size={14} />}
|
||||||
className="cancel-run"
|
className="periscope-btn secondary cancel-run"
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
if (listQueryKeyRef?.current) {
|
if (listQueryKeyRef?.current) {
|
||||||
queryClient.cancelQueries(listQueryKeyRef.current);
|
queryClient.cancelQueries(listQueryKeyRef.current);
|
||||||
@@ -56,18 +59,18 @@ export default function RightToolbarActions({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Cancel Run
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
className="right-toolbar"
|
className="periscope-btn primary run-query-btn"
|
||||||
disabled={isLoadingQueries}
|
disabled={isLoadingQueries}
|
||||||
onClick={onStageRunQuery}
|
onClick={onStageRunQuery}
|
||||||
icon={<Play size={14} />}
|
icon={<Play size={14} />}
|
||||||
>
|
>
|
||||||
Stage & Run Query
|
Run Query
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -78,7 +78,17 @@
|
|||||||
.right-toolbar {
|
.right-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: var(--bg-robin-600);
|
gap: 4px;
|
||||||
|
|
||||||
|
.query-in-progress-container {
|
||||||
|
.cancel-run {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-query-btn {
|
||||||
|
width: 136px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-actions {
|
.right-actions {
|
||||||
@@ -86,15 +96,15 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-container {
|
.query-in-progress-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 4px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.loading-btn {
|
.query-in-progress-btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 33px;
|
height: 32px;
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -146,8 +156,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.loading-container {
|
.query-in-progress-container {
|
||||||
.loading-btn {
|
.query-in-progress-btn {
|
||||||
background: var(--bg-vanilla-300) !important;
|
background: var(--bg-vanilla-300) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user