mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-20 23:30:31 +01:00
Compare commits
17 Commits
SIG-3098
...
feat/warni
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
702831e26b | ||
|
|
11a7512a25 | ||
|
|
f925a15f30 | ||
|
|
5c6346df11 | ||
|
|
a3fe2c2589 | ||
|
|
2719c9b6a7 | ||
|
|
b4282be3ac | ||
|
|
860c3e70ac | ||
|
|
15fe3a7163 | ||
|
|
30b98582b4 | ||
|
|
99b9e27bca | ||
|
|
7e72f501a7 | ||
|
|
3d8bfd47fa | ||
|
|
97d7b81a73 | ||
|
|
f01b00bd5c | ||
|
|
120317321c | ||
|
|
895e92893c |
17
.github/CODEOWNERS
vendored
17
.github/CODEOWNERS
vendored
@@ -64,6 +64,21 @@
|
||||
|
||||
# Dashboard Owners
|
||||
|
||||
/frontend/src/hooks/dashboard/ @SigNoz/pulse-frontend
|
||||
|
||||
## Dashboard List
|
||||
|
||||
/frontend/src/pages/DashboardsListPage/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/ListOfDashboard/ @SigNoz/pulse-frontend
|
||||
|
||||
## Dashboard Page
|
||||
|
||||
/frontend/src/pages/DashboardPage/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/NewDashboard/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/DashboardContainer/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/GridCardLayout/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/NewWidget/ @SigNoz/pulse-frontend
|
||||
|
||||
## Public Dashboard Page
|
||||
|
||||
/frontend/src/pages/PublicDashboard/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/PublicDashboardContainer/ @SigNoz/pulse-frontend
|
||||
|
||||
@@ -163,7 +163,7 @@ func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.U
|
||||
}
|
||||
|
||||
err = module.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
err := module.DeletePublic(ctx, orgID, id)
|
||||
err := module.deletePublic(ctx, orgID, id)
|
||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
return err
|
||||
}
|
||||
@@ -263,3 +263,35 @@ func (module *module) Update(ctx context.Context, orgID valuer.UUID, id valuer.U
|
||||
func (module *module) LockUnlock(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, role types.Role, lock bool) error {
|
||||
return module.pkgDashboardModule.LockUnlock(ctx, orgID, id, updatedBy, role, lock)
|
||||
}
|
||||
|
||||
func (module *module) deletePublic(ctx context.Context, orgID valuer.UUID, dashboardID valuer.UUID) error {
|
||||
publicDashboard, err := module.store.GetPublic(ctx, dashboardID.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
role, err := module.role.GetOrCreate(ctx, roletypes.NewRole(roletypes.AnonymousUserRoleName, roletypes.AnonymousUserRoleDescription, roletypes.RoleTypeManaged.StringValue(), orgID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deletionObject := authtypes.MustNewObject(
|
||||
authtypes.Resource{
|
||||
Name: dashboardtypes.TypeableMetaResourcePublicDashboard.Name(),
|
||||
Type: authtypes.TypeMetaResource,
|
||||
},
|
||||
authtypes.MustNewSelector(authtypes.TypeMetaResource, publicDashboard.ID.String()),
|
||||
)
|
||||
|
||||
err = module.role.PatchObjects(ctx, orgID, role.ID, authtypes.RelationRead, nil, []*authtypes.Object{deletionObject})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = module.store.DeletePublic(ctx, dashboardID.StringValue())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
102
frontend/src/components/CustomTimePicker/CalendarContainer.tsx
Normal file
102
frontend/src/components/CustomTimePicker/CalendarContainer.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { Calendar } from '@signozhq/calendar';
|
||||
import { Button } from 'antd';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import dayjs from 'dayjs';
|
||||
import { CalendarIcon, Check, X } from 'lucide-react';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
|
||||
import { DateRange } from './CustomTimePickerPopoverContent';
|
||||
|
||||
function CalendarContainer({
|
||||
dateRange,
|
||||
onSelectDateRange,
|
||||
onCancel,
|
||||
onApply,
|
||||
}: {
|
||||
dateRange: DateRange;
|
||||
onSelectDateRange: (dateRange: DateRange) => void;
|
||||
onCancel: () => void;
|
||||
onApply: () => void;
|
||||
}): JSX.Element {
|
||||
const { timezone } = useTimezone();
|
||||
|
||||
// this is to override the default behavior of the shadcn calendar component
|
||||
// if a range is already selected, clicking on a date will reset selection and set the new date as the start date
|
||||
const handleSelect = (
|
||||
_selected: DateRange | undefined,
|
||||
clickedDate?: Date,
|
||||
): void => {
|
||||
if (!clickedDate) {
|
||||
return;
|
||||
}
|
||||
|
||||
// No dates selected → start new
|
||||
if (!dateRange?.from) {
|
||||
onSelectDateRange({ from: clickedDate });
|
||||
return;
|
||||
}
|
||||
|
||||
// Only start selected → complete the range
|
||||
if (dateRange.from && !dateRange.to) {
|
||||
if (clickedDate < dateRange.from) {
|
||||
onSelectDateRange({ from: clickedDate, to: dateRange.from });
|
||||
} else {
|
||||
onSelectDateRange({ from: dateRange.from, to: clickedDate });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
onSelectDateRange({ from: clickedDate, to: undefined });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="calendar-container">
|
||||
<div className="calendar-container-header">
|
||||
<CalendarIcon size={12} />
|
||||
<div className="calendar-container-header-title">
|
||||
{dayjs(dateRange?.from)
|
||||
.tz(timezone.value)
|
||||
.format(DATE_TIME_FORMATS.MONTH_DATE_SHORT)}{' '}
|
||||
-{' '}
|
||||
{dayjs(dateRange?.to)
|
||||
.tz(timezone.value)
|
||||
.format(DATE_TIME_FORMATS.MONTH_DATE_SHORT)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="calendar-container-body">
|
||||
<Calendar
|
||||
mode="range"
|
||||
required
|
||||
defaultMonth={dateRange?.from}
|
||||
selected={dateRange}
|
||||
disabled={{
|
||||
after: dayjs().toDate(),
|
||||
}}
|
||||
onSelect={handleSelect}
|
||||
/>
|
||||
|
||||
<div className="calendar-actions">
|
||||
<Button
|
||||
type="primary"
|
||||
className="periscope-btn secondary cancel-btn"
|
||||
onClick={onCancel}
|
||||
icon={<X size={12} />}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
className="periscope-btn primary apply-btn"
|
||||
onClick={onApply}
|
||||
icon={<Check size={12} />}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CalendarContainer;
|
||||
@@ -36,7 +36,6 @@
|
||||
}
|
||||
|
||||
.time-selection-dropdown-content {
|
||||
min-width: 172px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -48,18 +47,16 @@
|
||||
padding: 4px 8px;
|
||||
padding-left: 0px !important;
|
||||
|
||||
&.custom-time {
|
||||
input:not(:focus) {
|
||||
min-width: 280px;
|
||||
input {
|
||||
width: 280px;
|
||||
|
||||
&::placeholder {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: white;
|
||||
}
|
||||
|
||||
input:focus::placeholder {
|
||||
color: rgba($color: #ffffff, $alpha: 0.4);
|
||||
&:focus::placeholder {
|
||||
color: rgba($color: #ffffff, $alpha: 0.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,9 +172,26 @@
|
||||
}
|
||||
|
||||
.time-input-prefix {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 4px;
|
||||
border-radius: 3px;
|
||||
|
||||
width: 36px;
|
||||
|
||||
font-size: 11px;
|
||||
color: var(--bg-vanilla-400);
|
||||
background-color: var(--bg-ink-200);
|
||||
|
||||
&.is-live {
|
||||
background-color: transparent;
|
||||
color: var(--bg-forest-500);
|
||||
}
|
||||
|
||||
.live-dot-icon {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--bg-forest-500);
|
||||
animation: ripple 1s infinite;
|
||||
@@ -191,7 +205,7 @@
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.4);
|
||||
}
|
||||
70% {
|
||||
60% {
|
||||
box-shadow: 0 0 0 6px rgba(245, 158, 11, 0);
|
||||
}
|
||||
100% {
|
||||
@@ -251,6 +265,11 @@
|
||||
background: rgb(179 179 179 / 15%);
|
||||
}
|
||||
|
||||
.time-input-prefix {
|
||||
background-color: var(--bg-vanilla-300);
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.time-input-suffix-icon-badge {
|
||||
color: var(--bg-ink-100);
|
||||
background: rgb(179 179 179 / 15%);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
import './CustomTimePicker.styles.scss';
|
||||
|
||||
import { Input, Popover, Tooltip, Typography } from 'antd';
|
||||
import { Input, InputRef, Popover, Tooltip } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
@@ -13,10 +14,9 @@ import {
|
||||
RelativeDurationSuggestionOptions,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
import dayjs from 'dayjs';
|
||||
import { isValidTimeFormat } from 'lib/getMinMax';
|
||||
import { isValidShortHandDateTimeFormat } from 'lib/getMinMax';
|
||||
import { defaultTo, isFunction, noop } from 'lodash-es';
|
||||
import debounce from 'lodash-es/debounce';
|
||||
import { CheckCircle, ChevronDown, Clock } from 'lucide-react';
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import {
|
||||
ChangeEvent,
|
||||
@@ -25,20 +25,26 @@ import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { getTimeDifference, validateEpochRange } from 'utils/epochUtils';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
import { TimeRangeValidationResult, validateTimeRange } from 'utils/timeUtils';
|
||||
|
||||
import CustomTimePickerPopoverContent from './CustomTimePickerPopoverContent';
|
||||
|
||||
const maxAllowedMinTimeInMonths = 6;
|
||||
const maxAllowedMinTimeInMonths = 15;
|
||||
type ViewType = 'datetime' | 'timezone';
|
||||
const DEFAULT_VIEW: ViewType = 'datetime';
|
||||
|
||||
export enum CustomTimePickerInputStatus {
|
||||
SUCCESS = 'success',
|
||||
ERROR = 'error',
|
||||
UNSET = '',
|
||||
}
|
||||
|
||||
interface CustomTimePickerProps {
|
||||
onSelect: (value: string) => void;
|
||||
onError: (value: boolean) => void;
|
||||
@@ -64,6 +70,8 @@ interface CustomTimePickerProps {
|
||||
onExitLiveLogs?: () => void;
|
||||
/** When false, hides the "Recently Used" time ranges section */
|
||||
showRecentlyUsed?: boolean;
|
||||
minTime: number;
|
||||
maxTime: number;
|
||||
}
|
||||
|
||||
function CustomTimePicker({
|
||||
@@ -84,23 +92,24 @@ function CustomTimePicker({
|
||||
onExitLiveLogs,
|
||||
showLiveLogs,
|
||||
showRecentlyUsed = true,
|
||||
minTime,
|
||||
maxTime,
|
||||
}: CustomTimePickerProps): JSX.Element {
|
||||
const [
|
||||
selectedTimePlaceholderValue,
|
||||
setSelectedTimePlaceholderValue,
|
||||
] = useState('Select / Enter Time Range');
|
||||
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [inputStatus, setInputStatus] = useState<'' | 'error' | 'success'>('');
|
||||
const [inputErrorMessage, setInputErrorMessage] = useState<string | null>(
|
||||
null,
|
||||
const [inputStatus, setInputStatus] = useState<CustomTimePickerInputStatus>(
|
||||
CustomTimePickerInputStatus.UNSET,
|
||||
);
|
||||
const [inputErrorDetails, setInputErrorDetails] = useState<
|
||||
TimeRangeValidationResult['errorDetails'] | null
|
||||
>(null);
|
||||
const location = useLocation();
|
||||
const [isInputFocused, setIsInputFocused] = useState(false);
|
||||
|
||||
const inputRef = useRef<InputRef>(null);
|
||||
|
||||
const [activeView, setActiveView] = useState<ViewType>(DEFAULT_VIEW);
|
||||
|
||||
@@ -123,12 +132,48 @@ function CustomTimePicker({
|
||||
|
||||
const [isOpenedFromFooter, setIsOpenedFromFooter] = useState(false);
|
||||
|
||||
// function to get selected time in Last 1m, Last 2h, Last 3d, Last 4w format
|
||||
// 1m, 2h, 3d, 4w -> Last 1 minute, Last 2 hours, Last 3 days, Last 4 weeks
|
||||
const getSelectedTimeRangeLabelInRelativeFormat = (
|
||||
selectedTime: string,
|
||||
): string => {
|
||||
if (!selectedTime || selectedTime === 'custom') {
|
||||
return selectedTime || '';
|
||||
}
|
||||
|
||||
// Check if the format matches the relative time format (e.g., 1m, 2h, 3d, 4w)
|
||||
const match = selectedTime.match(/^(\d+)([mhdw])$/);
|
||||
if (!match) {
|
||||
// If it doesn't match the format, return as is
|
||||
return `Last ${selectedTime}`;
|
||||
}
|
||||
|
||||
const value = parseInt(match[1], 10);
|
||||
const unit = match[2];
|
||||
|
||||
// Map unit abbreviations to full words
|
||||
const unitMap: Record<string, { singular: string; plural: string }> = {
|
||||
m: { singular: 'minute', plural: 'minutes' },
|
||||
h: { singular: 'hour', plural: 'hours' },
|
||||
d: { singular: 'day', plural: 'days' },
|
||||
w: { singular: 'week', plural: 'weeks' },
|
||||
};
|
||||
|
||||
const unitLabel = value === 1 ? unitMap[unit].singular : unitMap[unit].plural;
|
||||
|
||||
return `Last ${value} ${unitLabel}`;
|
||||
};
|
||||
|
||||
const getSelectedTimeRangeLabel = (
|
||||
selectedTime: string,
|
||||
selectedTimeValue: string,
|
||||
): string => {
|
||||
if (!selectedTime) {
|
||||
return '';
|
||||
}
|
||||
|
||||
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)
|
||||
// TODO: 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) {
|
||||
@@ -164,42 +209,90 @@ function CustomTimePicker({
|
||||
}
|
||||
}
|
||||
|
||||
if (isValidTimeFormat(selectedTime)) {
|
||||
return selectedTime;
|
||||
if (isValidShortHandDateTimeFormat(selectedTime)) {
|
||||
return getSelectedTimeRangeLabelInRelativeFormat(selectedTime);
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const resetErrorStatus = (): void => {
|
||||
setInputStatus(CustomTimePickerInputStatus.UNSET);
|
||||
onError(false);
|
||||
setInputErrorDetails(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (showLiveLogs) {
|
||||
setSelectedTimePlaceholderValue('Live');
|
||||
setInputValue('Live');
|
||||
resetErrorStatus();
|
||||
} else {
|
||||
const value = getSelectedTimeRangeLabel(selectedTime, selectedValue);
|
||||
setSelectedTimePlaceholderValue(value);
|
||||
setInputValue(value);
|
||||
resetErrorStatus();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedTime, selectedValue, showLiveLogs]);
|
||||
|
||||
const hide = (): void => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const getInputPrefix = (): JSX.Element => {
|
||||
if (showLiveLogs) {
|
||||
return (
|
||||
<span className="time-input-prefix is-live">
|
||||
<span className="live-dot-icon" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const timeDifference = getTimeDifference(
|
||||
Number(minTime / 1000_000),
|
||||
Number(maxTime / 1000_000),
|
||||
);
|
||||
|
||||
return <span className="time-input-prefix">{timeDifference}</span>;
|
||||
};
|
||||
|
||||
const handleOpenChange = (newOpen: boolean): void => {
|
||||
setOpen(newOpen);
|
||||
|
||||
if (!newOpen) {
|
||||
setCustomDTPickerVisible?.(false);
|
||||
setActiveView('datetime');
|
||||
|
||||
if (showLiveLogs) {
|
||||
setSelectedTimePlaceholderValue('Live');
|
||||
setInputValue('Live');
|
||||
return;
|
||||
}
|
||||
|
||||
// set the input value to a relative format if the selected time is not custom
|
||||
const inputValue = getSelectedTimeRangeLabel(selectedTime, selectedValue);
|
||||
setInputValue(inputValue);
|
||||
}
|
||||
};
|
||||
|
||||
const debouncedHandleInputChange = debounce((inputValue): void => {
|
||||
const isValidFormat = /^(\d+)([mhdw])$/.test(inputValue);
|
||||
if (isValidFormat) {
|
||||
setInputStatus('success');
|
||||
onError(false);
|
||||
setInputErrorMessage(null);
|
||||
const handleInputChange = (event: ChangeEvent<HTMLInputElement>): void => {
|
||||
const inputValue = event.target.value;
|
||||
setInputValue(inputValue);
|
||||
|
||||
const match = inputValue.match(/^(\d+)([mhdw])$/);
|
||||
resetErrorStatus();
|
||||
};
|
||||
|
||||
const handleInputPressEnter = (): void => {
|
||||
// check if the entered time is in the format of 1m, 2h, 3d, 4w
|
||||
const isTimeDurationShortHandFormat = /^(\d+)([mhdw])$/.test(inputValue);
|
||||
|
||||
if (isTimeDurationShortHandFormat) {
|
||||
setInputStatus(CustomTimePickerInputStatus.SUCCESS);
|
||||
onError(false);
|
||||
setInputErrorDetails(null);
|
||||
|
||||
const match = inputValue.match(/^(\d+)([mhdw])$/) as RegExpMatchArray;
|
||||
|
||||
const value = parseInt(match[1], 10);
|
||||
const unit = match[2];
|
||||
@@ -230,9 +323,13 @@ function CustomTimePicker({
|
||||
}
|
||||
|
||||
if (minTime && (!minTime.isValid() || minTime < maxAllowedMinTime)) {
|
||||
setInputStatus('error');
|
||||
setInputStatus(CustomTimePickerInputStatus.ERROR);
|
||||
onError(true);
|
||||
setInputErrorMessage('Please enter time less than 6 months');
|
||||
setInputErrorDetails({
|
||||
message: `Please enter time less than ${maxAllowedMinTimeInMonths} months`,
|
||||
code: 'TIME_LESS_THAN_MAX_ALLOWED_TIME_IN_MONTHS',
|
||||
description: `Please enter time less than ${maxAllowedMinTimeInMonths} months`,
|
||||
});
|
||||
if (isFunction(onCustomTimeStatusUpdate)) {
|
||||
onCustomTimeStatusUpdate(true);
|
||||
}
|
||||
@@ -241,44 +338,64 @@ function CustomTimePicker({
|
||||
time: [minTime, currentTime],
|
||||
timeStr: inputValue,
|
||||
});
|
||||
|
||||
setOpen(false);
|
||||
}
|
||||
} else {
|
||||
setInputStatus('error');
|
||||
onError(true);
|
||||
setInputErrorMessage(null);
|
||||
if (isFunction(onCustomTimeStatusUpdate)) {
|
||||
onCustomTimeStatusUpdate(false);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}, 300);
|
||||
|
||||
const handleInputChange = (event: ChangeEvent<HTMLInputElement>): void => {
|
||||
const inputValue = event.target.value;
|
||||
// parse the input value to get the start and end time
|
||||
const [startTime, endTime] = inputValue.split(/\s[-–]\s/);
|
||||
|
||||
// check if startTime and endTime are epoch format
|
||||
const { isValid: isValidStartTime, range: epochRange } = validateEpochRange(
|
||||
Number(startTime),
|
||||
Number(endTime),
|
||||
);
|
||||
|
||||
if (isValidStartTime && epochRange?.startTime && epochRange?.endTime) {
|
||||
onCustomDateHandler?.([epochRange?.startTime, epochRange?.endTime]);
|
||||
|
||||
if (inputValue.length > 0) {
|
||||
setOpen(false);
|
||||
} else {
|
||||
setOpen(true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setInputValue(inputValue);
|
||||
const {
|
||||
isValid: isValidTimeRange,
|
||||
errorDetails,
|
||||
startTimeMs,
|
||||
endTimeMs,
|
||||
} = validateTimeRange(
|
||||
startTime,
|
||||
endTime,
|
||||
DATE_TIME_FORMATS.UK_DATETIME_SECONDS,
|
||||
);
|
||||
|
||||
// Call the debounced function with the input value
|
||||
debouncedHandleInputChange(inputValue);
|
||||
if (!isValidTimeRange) {
|
||||
setInputStatus(CustomTimePickerInputStatus.ERROR);
|
||||
onError(true);
|
||||
setInputErrorDetails(errorDetails || null);
|
||||
return;
|
||||
}
|
||||
|
||||
onCustomDateHandler?.([dayjs(startTimeMs), dayjs(endTimeMs)]);
|
||||
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleSelect = (label: string, value: string): void => {
|
||||
if (label === 'Custom') {
|
||||
if (value === 'custom') {
|
||||
setCustomDTPickerVisible?.(true);
|
||||
return;
|
||||
}
|
||||
|
||||
onSelect(value);
|
||||
setSelectedTimePlaceholderValue(label);
|
||||
setInputStatus('');
|
||||
onError(false);
|
||||
setInputErrorMessage(null);
|
||||
resetErrorStatus();
|
||||
setInputValue('');
|
||||
|
||||
if (value !== 'custom') {
|
||||
hide();
|
||||
}
|
||||
@@ -305,20 +422,48 @@ function CustomTimePicker({
|
||||
</div>
|
||||
);
|
||||
|
||||
const handleFocus = (): void => {
|
||||
setIsInputFocused(true);
|
||||
setActiveView('datetime');
|
||||
const handleOpen = (e: React.SyntheticEvent): void => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (showLiveLogs) {
|
||||
setOpen(true);
|
||||
setSelectedTimePlaceholderValue('Live');
|
||||
setInputValue('Live');
|
||||
return;
|
||||
}
|
||||
|
||||
setOpen(true);
|
||||
// reset the input status and error message as we reset the time to previous correct value
|
||||
resetErrorStatus();
|
||||
|
||||
const startTime = dayjs(minTime / 1000_000).format(
|
||||
DATE_TIME_FORMATS.UK_DATETIME_SECONDS,
|
||||
);
|
||||
const endTime = dayjs(maxTime / 1000_000).format(
|
||||
DATE_TIME_FORMATS.UK_DATETIME_SECONDS,
|
||||
);
|
||||
|
||||
setInputValue(`${startTime} - ${endTime}`);
|
||||
};
|
||||
|
||||
const handleBlur = (): void => {
|
||||
setIsInputFocused(false);
|
||||
const handleClose = (e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
setOpen(false);
|
||||
setCustomDTPickerVisible?.(false);
|
||||
|
||||
if (showLiveLogs) {
|
||||
setInputValue('Live');
|
||||
return;
|
||||
}
|
||||
|
||||
// set the input value to a relative format if the selected time is not custom
|
||||
const inputValue = getSelectedTimeRangeLabel(selectedTime, selectedValue);
|
||||
setInputValue(inputValue);
|
||||
};
|
||||
|
||||
// this is required as TopNav component wraps the components and we need to clear the state on path change
|
||||
useEffect(() => {
|
||||
setInputStatus('');
|
||||
onError(false);
|
||||
setInputErrorMessage(null);
|
||||
resetErrorStatus();
|
||||
setInputValue('');
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [location.pathname]);
|
||||
@@ -335,6 +480,10 @@ function CustomTimePicker({
|
||||
);
|
||||
};
|
||||
|
||||
const handleInputBlur = (): void => {
|
||||
resetErrorStatus();
|
||||
};
|
||||
|
||||
const getTooltipTitle = (): string => {
|
||||
if (selectedTime === 'custom' && inputValue === '' && !open) {
|
||||
return `${dayjs(minTime / 1000_000)
|
||||
@@ -349,27 +498,19 @@ function CustomTimePicker({
|
||||
return '';
|
||||
};
|
||||
|
||||
const getInputPrefix = (): JSX.Element => {
|
||||
if (showLiveLogs) {
|
||||
return (
|
||||
<div className="time-input-prefix">
|
||||
<div className="live-dot-icon" />
|
||||
</div>
|
||||
);
|
||||
// Focus and select input text when popover opens
|
||||
useEffect(() => {
|
||||
if (open && inputRef.current) {
|
||||
// Use setTimeout to wait for React to update the DOM and make input editable
|
||||
setTimeout(() => {
|
||||
const inputElement = inputRef.current?.input;
|
||||
if (inputElement) {
|
||||
inputElement.focus();
|
||||
inputElement.select();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="time-input-prefix">
|
||||
{inputValue && inputStatus === 'success' ? (
|
||||
<CheckCircle size={14} color="#51E7A8" />
|
||||
) : (
|
||||
<Tooltip title="Enter time in format (e.g., 1m, 2h, 3d, 4w)">
|
||||
<Clock size={14} className="cursor-pointer" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<div className="custom-time-picker">
|
||||
@@ -385,9 +526,10 @@ function CustomTimePicker({
|
||||
content={
|
||||
newPopover ? (
|
||||
<CustomTimePickerPopoverContent
|
||||
isLiveLogsEnabled={!!showLiveLogs}
|
||||
setIsOpen={setOpen}
|
||||
customDateTimeVisible={defaultTo(customDateTimeVisible, false)}
|
||||
setCustomDTPickerVisible={defaultTo(setCustomDTPickerVisible, noop)}
|
||||
customDateTimeVisible={defaultTo(customDateTimeVisible, false)}
|
||||
onCustomDateHandler={defaultTo(onCustomDateHandler, noop)}
|
||||
onSelectHandler={handleSelect}
|
||||
onGoLive={defaultTo(onGoLive, noop)}
|
||||
@@ -399,6 +541,10 @@ function CustomTimePicker({
|
||||
setIsOpenedFromFooter={setIsOpenedFromFooter}
|
||||
isOpenedFromFooter={isOpenedFromFooter}
|
||||
showRecentlyUsed={showRecentlyUsed}
|
||||
customDateTimeInputStatus={inputStatus}
|
||||
inputErrorDetails={inputErrorDetails}
|
||||
minTime={minTime}
|
||||
maxTime={maxTime}
|
||||
/>
|
||||
) : (
|
||||
content
|
||||
@@ -407,25 +553,32 @@ function CustomTimePicker({
|
||||
arrow={false}
|
||||
trigger="click"
|
||||
open={open}
|
||||
destroyTooltipOnHide
|
||||
onOpenChange={handleOpenChange}
|
||||
style={{
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
className="timeSelection-input"
|
||||
ref={inputRef}
|
||||
className={cx(
|
||||
'timeSelection-input',
|
||||
inputStatus === CustomTimePickerInputStatus.ERROR ? 'error' : '',
|
||||
)}
|
||||
type="text"
|
||||
status={inputValue && inputStatus === 'error' ? 'error' : ''}
|
||||
placeholder={
|
||||
isInputFocused
|
||||
? 'Time Format (1m or 2h or 3d or 4w)'
|
||||
: selectedTimePlaceholderValue
|
||||
status={
|
||||
inputValue && inputStatus === CustomTimePickerInputStatus.ERROR
|
||||
? 'error'
|
||||
: ''
|
||||
}
|
||||
readOnly={!open || showLiveLogs}
|
||||
placeholder={selectedTimePlaceholderValue}
|
||||
value={inputValue}
|
||||
onFocus={handleFocus}
|
||||
onClick={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
onFocus={handleOpen}
|
||||
onClick={handleOpen}
|
||||
onChange={handleInputChange}
|
||||
onPressEnter={handleInputPressEnter}
|
||||
onBlur={handleInputBlur}
|
||||
data-1p-ignore
|
||||
prefix={getInputPrefix()}
|
||||
suffix={
|
||||
@@ -435,24 +588,25 @@ function CustomTimePicker({
|
||||
<span>{activeTimezoneOffset}</span>
|
||||
</div>
|
||||
)}
|
||||
<ChevronDown
|
||||
size={14}
|
||||
className="cursor-pointer time-input-suffix-icon-badge"
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
handleViewChange('datetime');
|
||||
}}
|
||||
/>
|
||||
|
||||
{open ? (
|
||||
<ChevronUp
|
||||
size={14}
|
||||
className="cursor-pointer time-input-suffix-icon-badge"
|
||||
onClick={handleClose}
|
||||
/>
|
||||
) : (
|
||||
<ChevronDown
|
||||
size={14}
|
||||
className="cursor-pointer time-input-suffix-icon-badge"
|
||||
onClick={handleOpen}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Popover>
|
||||
</Tooltip>
|
||||
{inputStatus === 'error' && inputErrorMessage && (
|
||||
<Typography.Title level={5} className="valid-format-error">
|
||||
{inputErrorMessage}
|
||||
</Typography.Title>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Color } from '@signozhq/design-tokens';
|
||||
import { Button } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import DatePickerV2 from 'components/DatePickerV2/DatePickerV2';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
@@ -15,7 +14,7 @@ import {
|
||||
RelativeDurationSuggestionOptions,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
import dayjs from 'dayjs';
|
||||
import { Clock, PenLine } from 'lucide-react';
|
||||
import { Clock, PenLine, TriangleAlertIcon } from 'lucide-react';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import {
|
||||
Dispatch,
|
||||
@@ -27,10 +26,23 @@ import {
|
||||
} from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { getCustomTimeRanges } from 'utils/customTimeRangeUtils';
|
||||
import { TimeRangeValidationResult } from 'utils/timeUtils';
|
||||
|
||||
import CalendarContainer from './CalendarContainer';
|
||||
import { CustomTimePickerInputStatus } from './CustomTimePicker';
|
||||
import TimezonePicker from './TimezonePicker';
|
||||
|
||||
const TO_MILLISECONDS_FACTOR = 1000_000;
|
||||
|
||||
export type DateRange = {
|
||||
from: Date | undefined;
|
||||
to?: Date | undefined;
|
||||
};
|
||||
|
||||
interface CustomTimePickerPopoverContentProps {
|
||||
isLiveLogsEnabled: boolean;
|
||||
minTime: number;
|
||||
maxTime: number;
|
||||
options: any[];
|
||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||
customDateTimeVisible: boolean;
|
||||
@@ -48,6 +60,8 @@ interface CustomTimePickerPopoverContentProps {
|
||||
setIsOpenedFromFooter: Dispatch<SetStateAction<boolean>>;
|
||||
onExitLiveLogs: () => void;
|
||||
showRecentlyUsed: boolean;
|
||||
customDateTimeInputStatus: CustomTimePickerInputStatus;
|
||||
inputErrorDetails: TimeRangeValidationResult['errorDetails'] | null;
|
||||
}
|
||||
|
||||
interface RecentlyUsedDateTimeRange {
|
||||
@@ -58,8 +72,29 @@ interface RecentlyUsedDateTimeRange {
|
||||
to: string;
|
||||
}
|
||||
|
||||
const getDateRange = (
|
||||
minTime: number,
|
||||
maxTime: number,
|
||||
timezone: string,
|
||||
): DateRange => {
|
||||
const from = dayjs(minTime / TO_MILLISECONDS_FACTOR)
|
||||
.tz(timezone)
|
||||
.startOf('day')
|
||||
.toDate();
|
||||
|
||||
const to = dayjs(maxTime / TO_MILLISECONDS_FACTOR)
|
||||
.tz(timezone)
|
||||
.endOf('day')
|
||||
.toDate();
|
||||
|
||||
return { from, to };
|
||||
};
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function CustomTimePickerPopoverContent({
|
||||
isLiveLogsEnabled,
|
||||
minTime,
|
||||
maxTime,
|
||||
options,
|
||||
setIsOpen,
|
||||
customDateTimeVisible,
|
||||
@@ -74,6 +109,8 @@ function CustomTimePickerPopoverContent({
|
||||
setIsOpenedFromFooter,
|
||||
onExitLiveLogs,
|
||||
showRecentlyUsed = true,
|
||||
customDateTimeInputStatus = CustomTimePickerInputStatus.UNSET,
|
||||
inputErrorDetails,
|
||||
}: CustomTimePickerPopoverContentProps): JSX.Element {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
@@ -83,6 +120,9 @@ function CustomTimePickerPopoverContent({
|
||||
|
||||
const url = new URLSearchParams(window.location.search);
|
||||
|
||||
const { timezone } = useTimezone();
|
||||
const activeTimezoneOffset = timezone.offset;
|
||||
|
||||
let panelTypeFromURL = url.get(QueryParams.panelTypes);
|
||||
|
||||
try {
|
||||
@@ -94,8 +134,9 @@ function CustomTimePickerPopoverContent({
|
||||
const isLogsListView =
|
||||
panelTypeFromURL !== 'table' && panelTypeFromURL !== 'graph'; // we do not select list view in the url
|
||||
|
||||
const { timezone } = useTimezone();
|
||||
const activeTimezoneOffset = timezone.offset;
|
||||
const [dateRange, setDateRange] = useState<DateRange>(() =>
|
||||
getDateRange(minTime, maxTime, timezone.value),
|
||||
);
|
||||
|
||||
const [recentlyUsedTimeRanges, setRecentlyUsedTimeRanges] = useState<
|
||||
RecentlyUsedDateTimeRange[]
|
||||
@@ -177,36 +218,66 @@ function CustomTimePickerPopoverContent({
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const handleSelectDateRange = (dateRange: DateRange): void => {
|
||||
setDateRange(dateRange);
|
||||
};
|
||||
|
||||
const handleCalendarRangeApply = (): void => {
|
||||
if (dateRange) {
|
||||
const from = dayjs(dateRange.from)
|
||||
.tz(timezone.value)
|
||||
.startOf('day')
|
||||
.toDate();
|
||||
const to = dayjs(dateRange.to).tz(timezone.value).endOf('day').toDate();
|
||||
|
||||
onCustomDateHandler([dayjs(from), dayjs(to)]);
|
||||
}
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const handleCalendarRangeCancel = (): void => {
|
||||
setCustomDTPickerVisible(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="date-time-popover">
|
||||
{!customDateTimeVisible && (
|
||||
<div className="date-time-options">
|
||||
{isLogsExplorerPage && isLogsListView && (
|
||||
<Button className="data-time-live" type="text" onClick={handleGoLive}>
|
||||
Live
|
||||
</Button>
|
||||
)}
|
||||
{options.map((option) => (
|
||||
<Button
|
||||
type="text"
|
||||
key={option.label + option.value}
|
||||
onClick={(): void => {
|
||||
handleExitLiveLogs();
|
||||
onSelectHandler(option.label, option.value);
|
||||
}}
|
||||
className={cx(
|
||||
'date-time-options-btn',
|
||||
customDateTimeVisible
|
||||
? option.value === 'custom' && 'active'
|
||||
: selectedTime === option.value && 'active',
|
||||
)}
|
||||
>
|
||||
{option.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="date-time-options">
|
||||
{isLogsExplorerPage && isLogsListView && (
|
||||
<Button
|
||||
className={cx('data-time-live', isLiveLogsEnabled ? 'active' : '')}
|
||||
type="text"
|
||||
onClick={handleGoLive}
|
||||
>
|
||||
Live
|
||||
</Button>
|
||||
)}
|
||||
{options.map((option) => (
|
||||
<Button
|
||||
type="text"
|
||||
key={option.label + option.value}
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handleExitLiveLogs();
|
||||
onSelectHandler(option.label, option.value);
|
||||
}}
|
||||
className={cx(
|
||||
'date-time-options-btn',
|
||||
customDateTimeVisible
|
||||
? option.value === 'custom' && !isLiveLogsEnabled && 'active'
|
||||
: selectedTime === option.value && !isLiveLogsEnabled && 'active',
|
||||
)}
|
||||
>
|
||||
<span className="time-label">{option.label}</span>
|
||||
|
||||
{option.value !== 'custom' && option.value !== '1month' && (
|
||||
<span className="time-value">{option.value}</span>
|
||||
)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cx(
|
||||
'relative-date-time',
|
||||
@@ -214,19 +285,38 @@ function CustomTimePickerPopoverContent({
|
||||
)}
|
||||
>
|
||||
{customDateTimeVisible ? (
|
||||
<DatePickerV2
|
||||
onSetCustomDTPickerVisible={setCustomDTPickerVisible}
|
||||
setIsOpen={setIsOpen}
|
||||
onCustomDateHandler={onCustomDateHandler}
|
||||
<CalendarContainer
|
||||
dateRange={dateRange}
|
||||
onSelectDateRange={handleSelectDateRange}
|
||||
onCancel={handleCalendarRangeCancel}
|
||||
onApply={handleCalendarRangeApply}
|
||||
/>
|
||||
) : (
|
||||
<div className="time-selector-container">
|
||||
{customDateTimeInputStatus === CustomTimePickerInputStatus.ERROR &&
|
||||
inputErrorDetails && (
|
||||
<div className="input-error-message-container">
|
||||
<div className="input-error-message-title">
|
||||
<TriangleAlertIcon color={Color.BG_CHERRY_400} size={16} />
|
||||
<span className="input-error-message-text">
|
||||
{inputErrorDetails.message}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{inputErrorDetails.description && (
|
||||
<p className="input-error-message-description">
|
||||
{inputErrorDetails.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="relative-times-container">
|
||||
<div className="time-heading">RELATIVE TIMES</div>
|
||||
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
|
||||
</div>
|
||||
|
||||
{showRecentlyUsed && (
|
||||
{showRecentlyUsed && recentlyUsedTimeRanges.length > 0 && (
|
||||
<div className="recently-used-container">
|
||||
<div className="time-heading">RECENTLY USED</div>
|
||||
<div className="recently-used-range">
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
.date-picker-v2-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.custom-date-time-picker-v2 {
|
||||
padding: 12px;
|
||||
|
||||
.periscope-calendar {
|
||||
border-radius: 4px;
|
||||
border: none !important;
|
||||
background: none !important;
|
||||
padding: 8px 0 !important;
|
||||
}
|
||||
|
||||
.periscope-calendar-day {
|
||||
background: none !important;
|
||||
|
||||
&.periscope-calendar-today {
|
||||
&.text-accent-foreground {
|
||||
color: var(--bg-vanilla-100) !important;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
&:hover {
|
||||
background-color: var(--bg-robin-500) !important;
|
||||
color: var(--bg-vanilla-100) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-time-selector {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.time-input {
|
||||
border-radius: 4px;
|
||||
border: none !important;
|
||||
background: none !important;
|
||||
padding: 8px 4px !important;
|
||||
color: var(--bg-vanilla-100) !important;
|
||||
|
||||
&::-webkit-calendar-picker-indicator {
|
||||
display: none !important;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-date-time-picker-footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
|
||||
.next-btn {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
width: 80px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.invalid-date-range-tooltip {
|
||||
.ant-tooltip-inner {
|
||||
color: var(--bg-sakura-500) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.custom-date-time-picker-v2 {
|
||||
.periscope-calendar-day {
|
||||
&.periscope-calendar-today {
|
||||
&.text-accent-foreground {
|
||||
color: var(--bg-ink-500) !important;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
&:hover {
|
||||
background-color: var(--bg-robin-500) !important;
|
||||
color: var(--bg-ink-500) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-time-selector {
|
||||
.time-input {
|
||||
color: var(--bg-ink-500) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,311 +0,0 @@
|
||||
import './DatePickerV2.styles.scss';
|
||||
|
||||
import { Calendar } from '@signozhq/calendar';
|
||||
import { Input } from '@signozhq/input';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
|
||||
import { LexicalContext } from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { CornerUpLeft, MoveRight } from 'lucide-react';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { addCustomTimeRange } from 'utils/customTimeRangeUtils';
|
||||
|
||||
function DatePickerV2({
|
||||
onSetCustomDTPickerVisible,
|
||||
setIsOpen,
|
||||
onCustomDateHandler,
|
||||
}: {
|
||||
onSetCustomDTPickerVisible: (visible: boolean) => void;
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
onCustomDateHandler: (
|
||||
dateTimeRange: DateTimeRangeType,
|
||||
lexicalContext?: LexicalContext,
|
||||
) => void;
|
||||
}): JSX.Element {
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const timeInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const { timezone } = useTimezone();
|
||||
|
||||
const [selectedDateTimeFor, setSelectedDateTimeFor] = useState<'to' | 'from'>(
|
||||
'from',
|
||||
);
|
||||
|
||||
const [selectedFromDateTime, setSelectedFromDateTime] = useState<Dayjs | null>(
|
||||
dayjs(minTime / 1000_000).tz(timezone.value),
|
||||
);
|
||||
|
||||
const [selectedToDateTime, setSelectedToDateTime] = useState<Dayjs | null>(
|
||||
dayjs(maxTime / 1000_000).tz(timezone.value),
|
||||
);
|
||||
|
||||
const handleNext = (): void => {
|
||||
if (selectedDateTimeFor === 'to') {
|
||||
onCustomDateHandler([selectedFromDateTime, selectedToDateTime]);
|
||||
|
||||
addCustomTimeRange([selectedFromDateTime, selectedToDateTime]);
|
||||
|
||||
setIsOpen(false);
|
||||
onSetCustomDTPickerVisible(false);
|
||||
setSelectedDateTimeFor('from');
|
||||
} else {
|
||||
setSelectedDateTimeFor('to');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDateChange = (date: Date | undefined): void => {
|
||||
if (!date) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedDateTimeFor === 'from') {
|
||||
const prevFromDateTime = selectedFromDateTime;
|
||||
|
||||
const newDate = dayjs(date);
|
||||
|
||||
const updatedFromDateTime = prevFromDateTime
|
||||
? prevFromDateTime
|
||||
.year(newDate.year())
|
||||
.month(newDate.month())
|
||||
.date(newDate.date())
|
||||
: dayjs(date).tz(timezone.value);
|
||||
|
||||
setSelectedFromDateTime(updatedFromDateTime);
|
||||
} else {
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
setSelectedToDateTime((prev) => {
|
||||
const newDate = dayjs(date);
|
||||
|
||||
// Update only the date part, keeping time from existing state
|
||||
return prev
|
||||
? prev.year(newDate.year()).month(newDate.month()).date(newDate.date())
|
||||
: dayjs(date).tz(timezone.value);
|
||||
});
|
||||
}
|
||||
|
||||
// focus the time input
|
||||
timeInputRef?.current?.focus();
|
||||
};
|
||||
|
||||
const handleTimeChange = (time: string): void => {
|
||||
// time should have format HH:mm:ss
|
||||
if (!/^\d{2}:\d{2}:\d{2}$/.test(time)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedDateTimeFor === 'from') {
|
||||
setSelectedFromDateTime((prev) => {
|
||||
if (prev) {
|
||||
return prev
|
||||
.set('hour', parseInt(time.split(':')[0], 10))
|
||||
.set('minute', parseInt(time.split(':')[1], 10))
|
||||
.set('second', parseInt(time.split(':')[2], 10));
|
||||
}
|
||||
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
if (selectedDateTimeFor === 'to') {
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
setSelectedToDateTime((prev) => {
|
||||
if (prev) {
|
||||
return prev
|
||||
.set('hour', parseInt(time.split(':')[0], 10))
|
||||
.set('minute', parseInt(time.split(':')[1], 10))
|
||||
.set('second', parseInt(time.split(':')[2], 10));
|
||||
}
|
||||
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getDefaultMonth = (): Date => {
|
||||
let defaultDate = null;
|
||||
|
||||
if (selectedDateTimeFor === 'from') {
|
||||
defaultDate = selectedFromDateTime?.toDate();
|
||||
} else if (selectedDateTimeFor === 'to') {
|
||||
defaultDate = selectedToDateTime?.toDate();
|
||||
}
|
||||
|
||||
return defaultDate ?? new Date();
|
||||
};
|
||||
|
||||
const isValidRange = (): boolean => {
|
||||
if (selectedDateTimeFor === 'to') {
|
||||
return selectedToDateTime?.isAfter(selectedFromDateTime) ?? false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleBack = (): void => {
|
||||
setSelectedDateTimeFor('from');
|
||||
};
|
||||
|
||||
const handleHideCustomDTPicker = (): void => {
|
||||
onSetCustomDTPickerVisible(false);
|
||||
};
|
||||
|
||||
const handleSelectDateTimeFor = (selectedDateTimeFor: 'to' | 'from'): void => {
|
||||
setSelectedDateTimeFor(selectedDateTimeFor);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="date-picker-v2-container">
|
||||
<div className="date-time-custom-options-container">
|
||||
<div
|
||||
className="back-btn"
|
||||
onClick={handleHideCustomDTPicker}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter') {
|
||||
handleHideCustomDTPicker();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CornerUpLeft size={16} />
|
||||
<span>Back</span>
|
||||
</div>
|
||||
|
||||
<div className="date-time-custom-options">
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSelectDateTimeFor('from');
|
||||
}
|
||||
}}
|
||||
className={cx(
|
||||
'date-time-custom-option-from',
|
||||
selectedDateTimeFor === 'from' && 'active',
|
||||
)}
|
||||
onClick={(): void => {
|
||||
handleSelectDateTimeFor('from');
|
||||
}}
|
||||
>
|
||||
<div className="date-time-custom-option-from-title">FROM</div>
|
||||
<div className="date-time-custom-option-from-value">
|
||||
{selectedFromDateTime?.format('YYYY-MM-DD HH:mm:ss')}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSelectDateTimeFor('to');
|
||||
}
|
||||
}}
|
||||
className={cx(
|
||||
'date-time-custom-option-to',
|
||||
selectedDateTimeFor === 'to' && 'active',
|
||||
)}
|
||||
onClick={(): void => {
|
||||
handleSelectDateTimeFor('to');
|
||||
}}
|
||||
>
|
||||
<div className="date-time-custom-option-to-title">TO</div>
|
||||
<div className="date-time-custom-option-to-value">
|
||||
{selectedToDateTime?.format('YYYY-MM-DD HH:mm:ss')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="custom-date-time-picker-v2">
|
||||
<Calendar
|
||||
mode="single"
|
||||
required
|
||||
selected={
|
||||
selectedDateTimeFor === 'from'
|
||||
? selectedFromDateTime?.toDate()
|
||||
: selectedToDateTime?.toDate()
|
||||
}
|
||||
key={selectedDateTimeFor + selectedDateTimeFor}
|
||||
onSelect={handleDateChange}
|
||||
defaultMonth={getDefaultMonth()}
|
||||
disabled={(current): boolean => {
|
||||
if (selectedDateTimeFor === 'to') {
|
||||
// disable dates after today and before selectedFromDateTime
|
||||
const currentDay = dayjs(current);
|
||||
return currentDay.isAfter(dayjs()) || false;
|
||||
}
|
||||
|
||||
if (selectedDateTimeFor === 'from') {
|
||||
// disable dates after selectedToDateTime
|
||||
|
||||
return dayjs(current).isAfter(dayjs()) || false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}}
|
||||
className="rounded-md border"
|
||||
navLayout="after"
|
||||
/>
|
||||
|
||||
<div className="custom-time-selector">
|
||||
<label className="text-xs font-normal block" htmlFor="time-picker">
|
||||
Timestamp
|
||||
</label>
|
||||
|
||||
<MoveRight size={16} />
|
||||
|
||||
<div className="time-input-container">
|
||||
<Input
|
||||
type="time"
|
||||
ref={timeInputRef}
|
||||
className="time-input"
|
||||
value={
|
||||
selectedDateTimeFor === 'from'
|
||||
? selectedFromDateTime?.format('HH:mm:ss')
|
||||
: selectedToDateTime?.format('HH:mm:ss')
|
||||
}
|
||||
onChange={(e): void => handleTimeChange(e.target.value)}
|
||||
step="1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="custom-date-time-picker-footer">
|
||||
{selectedDateTimeFor === 'to' && (
|
||||
<Button
|
||||
className="periscope-btn secondary clear-btn"
|
||||
type="default"
|
||||
onClick={handleBack}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
<Tooltip
|
||||
title={
|
||||
!isValidRange() ? 'Invalid range: TO date should be after FROM date' : ''
|
||||
}
|
||||
overlayClassName="invalid-date-range-tooltip"
|
||||
>
|
||||
<Button
|
||||
className="periscope-btn primary next-btn"
|
||||
type="primary"
|
||||
onClick={handleNext}
|
||||
disabled={!isValidRange()}
|
||||
>
|
||||
{selectedDateTimeFor === 'from' ? 'Next' : 'Apply'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DatePickerV2;
|
||||
@@ -176,6 +176,8 @@ function HostMetricTraces({
|
||||
onTimeChange={handleTimeChange}
|
||||
defaultRelativeTime="5m"
|
||||
modalSelectedInterval={selectedInterval}
|
||||
modalInitialStartTime={timeRange.startTime * 1000}
|
||||
modalInitialEndTime={timeRange.endTime * 1000}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -87,6 +87,8 @@ function HostMetricLogsDetailedView({
|
||||
onTimeChange={handleTimeChange}
|
||||
defaultRelativeTime="5m"
|
||||
modalSelectedInterval={selectedInterval}
|
||||
modalInitialStartTime={timeRange.startTime * 1000}
|
||||
modalInitialEndTime={timeRange.endTime * 1000}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -211,6 +211,8 @@ function Metrics({
|
||||
defaultRelativeTime="5m"
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
modalSelectedInterval={selectedInterval}
|
||||
modalInitialStartTime={timeRange.startTime * 1000}
|
||||
modalInitialEndTime={timeRange.endTime * 1000}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { VirtuosoMockContext } from 'react-virtuoso';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
|
||||
import VariableItem from '../../../container/NewDashboard/DashboardVariablesSelection/VariableItem';
|
||||
import VariableItem from '../../../container/DashboardContainer/DashboardVariablesSelection/VariableItem';
|
||||
|
||||
// Mock the dashboard variables query
|
||||
jest.mock('api/dashboard/variables/dashboardVariablesQuery', () => ({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { uniqueOptions } from 'container/NewDashboard/DashboardVariablesSelection/util';
|
||||
import { uniqueOptions } from 'container/DashboardContainer/DashboardVariablesSelection/util';
|
||||
|
||||
import { OptionData } from './types';
|
||||
|
||||
|
||||
@@ -1,47 +1,3 @@
|
||||
.settings-container-root {
|
||||
.ant-drawer-wrapper-body {
|
||||
border-left: 1px solid var(--bg-slate-500);
|
||||
background: var(--bg-ink-400);
|
||||
box-shadow: -4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
|
||||
.ant-drawer-header {
|
||||
height: 48px;
|
||||
border-bottom: 1px solid var(--bg-slate-500);
|
||||
padding: 14px 14px 14px 11px;
|
||||
|
||||
.ant-drawer-header-title {
|
||||
gap: 16px;
|
||||
|
||||
.ant-drawer-title {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
padding-left: 16px;
|
||||
border-left: 1px solid #161922;
|
||||
}
|
||||
|
||||
.ant-drawer-close {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin-inline-end: 0px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-drawer-body {
|
||||
padding: 16px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-description-container {
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
@@ -576,28 +532,6 @@
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.settings-container-root {
|
||||
.ant-drawer-wrapper-body {
|
||||
border-left: 1px solid var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-100);
|
||||
|
||||
.ant-drawer-header {
|
||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||
|
||||
.ant-drawer-header-title {
|
||||
.ant-drawer-title {
|
||||
color: var(--bg-ink-400);
|
||||
border-left: 1px solid var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
.ant-drawer-close {
|
||||
color: var(--bg-ink-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-description-container {
|
||||
color: var(--bg-ink-400);
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
.settings-container-root {
|
||||
.ant-drawer-wrapper-body {
|
||||
border-left: 1px solid var(--bg-slate-500);
|
||||
background: var(--bg-ink-400);
|
||||
box-shadow: -4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
|
||||
.ant-drawer-header {
|
||||
height: 48px;
|
||||
border-bottom: 1px solid var(--bg-slate-500);
|
||||
padding: 14px 14px 14px 11px;
|
||||
|
||||
.ant-drawer-header-title {
|
||||
gap: 16px;
|
||||
|
||||
.ant-drawer-title {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
padding-left: 16px;
|
||||
border-left: 1px solid var(--bg-slate-500);
|
||||
}
|
||||
|
||||
.ant-drawer-close {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin-inline-end: 0px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-drawer-body {
|
||||
padding: 16px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.settings-container-root {
|
||||
.ant-drawer-wrapper-body {
|
||||
border-left: 1px solid var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-100);
|
||||
|
||||
.ant-drawer-header {
|
||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||
|
||||
.ant-drawer-header-title {
|
||||
.ant-drawer-title {
|
||||
color: var(--bg-ink-400);
|
||||
border-left: 1px solid var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
.ant-drawer-close {
|
||||
color: var(--bg-ink-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import './SettingsDrawer.styles.scss';
|
||||
|
||||
import { Drawer } from 'antd';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { memo, PropsWithChildren, ReactElement } from 'react';
|
||||
|
||||
type SettingsDrawerProps = PropsWithChildren<{
|
||||
drawerTitle: string;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}>;
|
||||
|
||||
function SettingsDrawer({
|
||||
children,
|
||||
drawerTitle,
|
||||
isOpen,
|
||||
onClose,
|
||||
}: SettingsDrawerProps): JSX.Element {
|
||||
return (
|
||||
<Drawer
|
||||
title={drawerTitle}
|
||||
placement="right"
|
||||
width="50%"
|
||||
onClose={onClose}
|
||||
open={isOpen}
|
||||
rootClassName="settings-container-root"
|
||||
>
|
||||
{/* Need to type cast because of OverlayScrollbar type definition. We should be good once we remove it. */}
|
||||
<OverlayScrollbar>{children as ReactElement}</OverlayScrollbar>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(SettingsDrawer);
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ConfigureIcon from 'assets/Integrations/ConfigureIcon';
|
||||
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
|
||||
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
@@ -40,7 +41,7 @@ import {
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { sortLayout } from 'providers/Dashboard/util';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { FullScreenHandle } from 'react-full-screen';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -52,9 +53,11 @@ import { ComponentTypes } from 'utils/permission';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import DashboardGraphSlider from '../ComponentsSlider';
|
||||
import DashboardSettings from '../DashboardSettings';
|
||||
import { Base64Icons } from '../DashboardSettings/General/utils';
|
||||
import DashboardVariableSelection from '../DashboardVariablesSelection';
|
||||
import SettingsDrawer from './SettingsDrawer';
|
||||
import { VariablesSettingsTab } from './types';
|
||||
import { DEFAULT_ROW_NAME, downloadObjectAsJson } from './utils';
|
||||
|
||||
interface DashboardDescriptionProps {
|
||||
@@ -101,6 +104,11 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
handleDashboardLockToggle,
|
||||
} = useDashboard();
|
||||
|
||||
const variablesSettingsTabHandle = useRef<VariablesSettingsTab>(null);
|
||||
const [isSettingsDrawerOpen, setIsSettingsDrawerOpen] = useState<boolean>(
|
||||
false,
|
||||
);
|
||||
|
||||
const { isCloudUser, isEnterpriseSelfHostedUser } = useGetTenantLicense();
|
||||
|
||||
const isPublicDashboardEnabled = isCloudUser || isEnterpriseSelfHostedUser;
|
||||
@@ -340,6 +348,18 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
publicDashboardResponse?.data,
|
||||
]);
|
||||
|
||||
const onConfigureClick = useCallback((): void => {
|
||||
setIsSettingsDrawerOpen(true);
|
||||
}, []);
|
||||
|
||||
const onSettingsDrawerClose = useCallback((): void => {
|
||||
setIsSettingsDrawerOpen(false);
|
||||
// good use case for a state library like Jotai
|
||||
if (variablesSettingsTabHandle.current) {
|
||||
variablesSettingsTabHandle.current.resetState();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Card className="dashboard-description-container">
|
||||
<div className="dashboard-header">
|
||||
@@ -504,7 +524,26 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
/>
|
||||
</Popover>
|
||||
{!isDashboardLocked && editDashboard && (
|
||||
<SettingsDrawer drawerTitle="Dashboard Configuration" />
|
||||
<>
|
||||
<Button
|
||||
type="text"
|
||||
className="configure-button"
|
||||
icon={<ConfigureIcon />}
|
||||
data-testid="show-drawer"
|
||||
onClick={onConfigureClick}
|
||||
>
|
||||
Configure
|
||||
</Button>
|
||||
<SettingsDrawer
|
||||
drawerTitle="Dashboard Configuration"
|
||||
isOpen={isSettingsDrawerOpen}
|
||||
onClose={onSettingsDrawerClose}
|
||||
>
|
||||
<DashboardSettings
|
||||
variablesSettingsTabHandle={variablesSettingsTabHandle}
|
||||
/>
|
||||
</SettingsDrawer>
|
||||
</>
|
||||
)}
|
||||
{!isDashboardLocked && addPanelPermission && (
|
||||
<Button
|
||||
@@ -0,0 +1,7 @@
|
||||
import { MutableRefObject } from 'react';
|
||||
|
||||
export interface VariablesSettingsTab {
|
||||
resetState: () => void;
|
||||
}
|
||||
|
||||
export type VariablesSettingsTabHandle = MutableRefObject<VariablesSettingsTab | null>;
|
||||
@@ -14,7 +14,8 @@ import { arrayMove, SortableContext, useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { Button, Modal, Row, Space, Table, Typography } from 'antd';
|
||||
import { RowProps } from 'antd/lib';
|
||||
import { convertVariablesToDbFormat } from 'container/NewDashboard/DashboardVariablesSelection/util';
|
||||
import { VariablesSettingsTabHandle } from 'container/DashboardContainer/DashboardDescription/types';
|
||||
import { convertVariablesToDbFormat } from 'container/DashboardContainer/DashboardVariablesSelection/util';
|
||||
import { useAddDynamicVariableToPanels } from 'hooks/dashboard/useAddDynamicVariableToPanels';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
@@ -77,10 +78,10 @@ function TableRow({ children, ...props }: RowProps): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
function VariablesSetting({
|
||||
variableViewModeRef,
|
||||
function VariablesSettings({
|
||||
variablesSettingsTabHandle,
|
||||
}: {
|
||||
variableViewModeRef: React.MutableRefObject<(() => void) | undefined>;
|
||||
variablesSettingsTabHandle: VariablesSettingsTabHandle;
|
||||
}): JSX.Element {
|
||||
const variableToDelete = useRef<IDashboardVariable | null>(null);
|
||||
const [deleteVariableModal, setDeleteVariableModal] = useState(false);
|
||||
@@ -126,11 +127,13 @@ function VariablesSetting({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (variableViewModeRef) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
variableViewModeRef.current = onDoneVariableViewMode;
|
||||
}
|
||||
}, [variableViewModeRef]);
|
||||
if (!variablesSettingsTabHandle) return;
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
variablesSettingsTabHandle.current = {
|
||||
resetState: onDoneVariableViewMode,
|
||||
};
|
||||
}, [variablesSettingsTabHandle]);
|
||||
|
||||
const updateMutation = useUpdateDashboard();
|
||||
|
||||
@@ -510,4 +513,4 @@ function VariablesSetting({
|
||||
);
|
||||
}
|
||||
|
||||
export default VariablesSetting;
|
||||
export default VariablesSettings;
|
||||
@@ -1,7 +1,7 @@
|
||||
import './GeneralSettings.styles.scss';
|
||||
|
||||
import { Col, Input, Select, Space, Typography } from 'antd';
|
||||
import AddTags from 'container/NewDashboard/DashboardSettings/General/AddTags';
|
||||
import AddTags from 'container/DashboardContainer/DashboardSettings/General/AddTags';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { Check, X } from 'lucide-react';
|
||||
@@ -6,14 +6,15 @@ import { Braces, Globe, Table } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
|
||||
import { VariablesSettingsTabHandle } from '../DashboardDescription/types';
|
||||
import DashboardVariableSettings from './DashboardVariableSettings';
|
||||
import GeneralDashboardSettings from './General';
|
||||
import PublicDashboardSetting from './PublicDashboard';
|
||||
import VariablesSetting from './Variables';
|
||||
|
||||
function DashboardSettingsContent({
|
||||
variableViewModeRef,
|
||||
function DashboardSettings({
|
||||
variablesSettingsTabHandle,
|
||||
}: {
|
||||
variableViewModeRef: React.MutableRefObject<(() => void) | undefined>;
|
||||
variablesSettingsTabHandle: VariablesSettingsTabHandle;
|
||||
}): JSX.Element {
|
||||
const { user } = useAppContext();
|
||||
const { isCloudUser, isEnterpriseSelfHostedUser } = useGetTenantLicense();
|
||||
@@ -63,7 +64,11 @@ function DashboardSettingsContent({
|
||||
</Button>
|
||||
),
|
||||
key: 'variables',
|
||||
children: <VariablesSetting variableViewModeRef={variableViewModeRef} />,
|
||||
children: (
|
||||
<DashboardVariableSettings
|
||||
variablesSettingsTabHandle={variablesSettingsTabHandle}
|
||||
/>
|
||||
),
|
||||
},
|
||||
...(enablePublicDashboard ? [publicDashboardItem] : []),
|
||||
];
|
||||
@@ -71,4 +76,4 @@ function DashboardSettingsContent({
|
||||
return <Tabs items={items} animated className="settings-tabs" />;
|
||||
}
|
||||
|
||||
export default DashboardSettingsContent;
|
||||
export default DashboardSettings;
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from 'types/api/dashboard/getAll';
|
||||
|
||||
import { useAddDynamicVariableToPanels } from '../../../hooks/dashboard/useAddDynamicVariableToPanels';
|
||||
import { WidgetSelector } from '../DashboardSettings/Variables/VariableItem/WidgetSelector';
|
||||
import { WidgetSelector } from '../DashboardSettings/DashboardVariableSettings/VariableItem/WidgetSelector';
|
||||
|
||||
// Mock scrollIntoView since it's not available in JSDOM
|
||||
window.HTMLElement.prototype.scrollIntoView = jest.fn();
|
||||
@@ -5,7 +5,7 @@ import { Provider } from 'react-redux';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
|
||||
import VariableItem from '../DashboardSettings/Variables/VariableItem/VariableItem';
|
||||
import VariableItem from '../DashboardSettings/DashboardVariableSettings/VariableItem/VariableItem';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('api/dashboard/variables/dashboardVariablesQuery');
|
||||
@@ -4,11 +4,14 @@ import './DashboardEmptyState.styles.scss';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import SettingsDrawer from 'container/NewDashboard/DashboardDescription/SettingsDrawer';
|
||||
import ConfigureIcon from 'assets/Integrations/ConfigureIcon';
|
||||
import SettingsDrawer from 'container/DashboardContainer/DashboardDescription/SettingsDrawer';
|
||||
import { VariablesSettingsTab } from 'container/DashboardContainer/DashboardDescription/types';
|
||||
import DashboardSettings from 'container/DashboardContainer/DashboardSettings';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { ROLES, USER_ROLES } from 'types/roles';
|
||||
import { ComponentTypes } from 'utils/permission';
|
||||
|
||||
@@ -20,6 +23,11 @@ export default function DashboardEmptyState(): JSX.Element {
|
||||
setSelectedRowWidgetId,
|
||||
} = useDashboard();
|
||||
|
||||
const variablesSettingsTabHandle = useRef<VariablesSettingsTab>(null);
|
||||
const [isSettingsDrawerOpen, setIsSettingsDrawerOpen] = useState<boolean>(
|
||||
false,
|
||||
);
|
||||
|
||||
const { user } = useAppContext();
|
||||
let permissions: ComponentTypes[] = ['add_panel'];
|
||||
|
||||
@@ -44,6 +52,19 @@ export default function DashboardEmptyState(): JSX.Element {
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [handleToggleDashboardSlider]);
|
||||
|
||||
const onConfigureClick = useCallback((): void => {
|
||||
setIsSettingsDrawerOpen(true);
|
||||
}, []);
|
||||
|
||||
const onSettingsDrawerClose = useCallback((): void => {
|
||||
setIsSettingsDrawerOpen(false);
|
||||
|
||||
if (variablesSettingsTabHandle.current) {
|
||||
variablesSettingsTabHandle.current.resetState();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="dashboard-empty-state">
|
||||
<div className="dashboard-content">
|
||||
@@ -77,7 +98,26 @@ export default function DashboardEmptyState(): JSX.Element {
|
||||
Give it a name, add description, tags and variables
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<SettingsDrawer drawerTitle="Dashboard Configuration" />
|
||||
{/* This Empty State needs to be consolidated. The SettingsDrawer should be global to the
|
||||
whole dashboard page instead of confined to this Empty State */}
|
||||
<Button
|
||||
type="text"
|
||||
className="configure-button"
|
||||
icon={<ConfigureIcon />}
|
||||
data-testid="show-drawer"
|
||||
onClick={onConfigureClick}
|
||||
>
|
||||
Configure
|
||||
</Button>
|
||||
<SettingsDrawer
|
||||
drawerTitle="Dashboard Configuration"
|
||||
isOpen={isSettingsDrawerOpen}
|
||||
onClose={onSettingsDrawerClose}
|
||||
>
|
||||
<DashboardSettings
|
||||
variablesSettingsTabHandle={variablesSettingsTabHandle}
|
||||
/>
|
||||
</SettingsDrawer>
|
||||
</div>
|
||||
<div className="actions-1">
|
||||
<div className="actions-add-panel">
|
||||
|
||||
@@ -3,7 +3,7 @@ import './PanelTypeSelector.scss';
|
||||
import { Select, Typography } from 'antd';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import GraphTypes from 'container/NewDashboard/ComponentsSlider/menuItems';
|
||||
import GraphTypes from 'container/DashboardContainer/ComponentsSlider/menuItems';
|
||||
import { handleQueryChange } from 'container/NewWidget/utils';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
@@ -10,7 +10,7 @@ import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import { DEFAULT_ROW_NAME } from 'container/NewDashboard/DashboardDescription/utils';
|
||||
import { DEFAULT_ROW_NAME } from 'container/DashboardContainer/DashboardDescription/utils';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { createDynamicVariableToWidgetsMap } from 'hooks/dashboard/utils';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
|
||||
@@ -17,7 +17,7 @@ jest.mock('lib/getMinMax', () => ({
|
||||
default: jest.fn().mockImplementation(() => ({
|
||||
minTime: 1713734400000,
|
||||
maxTime: 1713738000000,
|
||||
isValidTimeFormat: jest.fn().mockReturnValue(true),
|
||||
isValidShortHandDateTimeFormat: jest.fn().mockReturnValue(true),
|
||||
})),
|
||||
}));
|
||||
jest.mock('components/CustomTimePicker/CustomTimePicker', () => ({
|
||||
|
||||
@@ -266,6 +266,8 @@ export default function Events({
|
||||
onTimeChange={handleTimeChange}
|
||||
defaultRelativeTime="5m"
|
||||
modalSelectedInterval={selectedInterval}
|
||||
modalInitialStartTime={timeRange.startTime * 1000}
|
||||
modalInitialEndTime={timeRange.endTime * 1000}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -93,6 +93,8 @@ function EntityLogsDetailedView({
|
||||
onTimeChange={handleTimeChange}
|
||||
defaultRelativeTime="5m"
|
||||
modalSelectedInterval={selectedInterval}
|
||||
modalInitialStartTime={timeRange.startTime * 1000}
|
||||
modalInitialEndTime={timeRange.endTime * 1000}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -258,6 +258,8 @@ function EntityMetrics<T>({
|
||||
defaultRelativeTime="5m"
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
modalSelectedInterval={selectedInterval}
|
||||
modalInitialStartTime={timeRange.startTime * 1000}
|
||||
modalInitialEndTime={timeRange.endTime * 1000}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -188,6 +188,8 @@ function EntityTraces({
|
||||
onTimeChange={handleTimeChange}
|
||||
defaultRelativeTime="5m"
|
||||
modalSelectedInterval={selectedInterval}
|
||||
modalInitialStartTime={timeRange.startTime * 1000}
|
||||
modalInitialEndTime={timeRange.endTime * 1000}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -62,7 +62,7 @@ const setupCommonMocks = (): void => {
|
||||
minTime: 1713734400000,
|
||||
maxTime: 1713738000000,
|
||||
})),
|
||||
isValidTimeFormat: jest.fn().mockReturnValue(true),
|
||||
isValidShortHandDateTimeFormat: jest.fn().mockReturnValue(true),
|
||||
}));
|
||||
|
||||
jest.spyOn(appContextHooks, 'useAppContext').mockReturnValue({
|
||||
|
||||
@@ -28,9 +28,9 @@ import cx from 'classnames';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { sanitizeDashboardData } from 'container/NewDashboard/DashboardDescription';
|
||||
import { downloadObjectAsJson } from 'container/NewDashboard/DashboardDescription/utils';
|
||||
import { Base64Icons } from 'container/NewDashboard/DashboardSettings/General/utils';
|
||||
import { sanitizeDashboardData } from 'container/DashboardContainer/DashboardDescription';
|
||||
import { downloadObjectAsJson } from 'container/DashboardContainer/DashboardDescription/utils';
|
||||
import { Base64Icons } from 'container/DashboardContainer/DashboardSettings/General/utils';
|
||||
import dayjs from 'dayjs';
|
||||
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
|
||||
@@ -20,7 +20,7 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
|
||||
import { ExternalLink, Github, MonitorDot, MoveRight, X } from 'lucide-react';
|
||||
import { ExternalLink, Github, MonitorDot, MoveRight } from 'lucide-react';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
// #TODO: Lucide will be removing brand icons like GitHub in the future. In that case, we can use Simple Icons. https://simpleicons.org/
|
||||
// See more: https://github.com/lucide-icons/lucide/issues/94
|
||||
@@ -151,7 +151,10 @@ function ImportJSON({
|
||||
wrapClassName="import-json-modal"
|
||||
open={isImportJSONModalVisible}
|
||||
centered
|
||||
closable={false}
|
||||
closable
|
||||
keyboard
|
||||
maskClosable
|
||||
onCancel={onCancelHandler}
|
||||
destroyOnClose
|
||||
width="60vw"
|
||||
footer={
|
||||
@@ -223,8 +226,6 @@ function ImportJSON({
|
||||
<div className="import-json-content-container">
|
||||
<div className="import-json-content-header">
|
||||
<Typography.Text>{t('import_json')}</Typography.Text>
|
||||
|
||||
<X size={14} className="periscope-btn ghost" onClick={onCancelHandler} />
|
||||
</div>
|
||||
|
||||
<MEditor
|
||||
|
||||
@@ -92,6 +92,10 @@ function TableView({
|
||||
}
|
||||
});
|
||||
}
|
||||
// pin trace_id by default when present
|
||||
if (logData?.trace_id) {
|
||||
pinnedAttributes.trace_id = true;
|
||||
}
|
||||
|
||||
setPinnedAttributes(pinnedAttributes);
|
||||
}, [
|
||||
|
||||
@@ -518,15 +518,15 @@ describe('Logs Explorer -> stage and run query', () => {
|
||||
const initialStart = initialPayload.start;
|
||||
const initialEnd = initialPayload.end;
|
||||
|
||||
// Click the Stage & Run Query button
|
||||
// Click the Run Query button
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
await user.click(
|
||||
screen.getByRole('button', {
|
||||
name: /stage & run query/i,
|
||||
name: /run query/i,
|
||||
}),
|
||||
);
|
||||
|
||||
// Wait for additional API calls to be made after clicking Stage & Run Query
|
||||
// Wait for additional API calls to be made after clicking Run Query
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(capturedPayloads.length).toBeGreaterThan(1);
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import './Description.styles.scss';
|
||||
|
||||
import { Button } from 'antd';
|
||||
import ConfigureIcon from 'assets/Integrations/ConfigureIcon';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
import DashboardSettingsContent from '../DashboardSettings';
|
||||
import { DrawerContainer } from './styles';
|
||||
|
||||
function SettingsDrawer({ drawerTitle }: { drawerTitle: string }): JSX.Element {
|
||||
const [visible, setVisible] = useState<boolean>(false);
|
||||
|
||||
const variableViewModeRef = useRef<() => void>();
|
||||
|
||||
const showDrawer = (): void => {
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
const handleClose = (): void => {
|
||||
setVisible(false);
|
||||
variableViewModeRef?.current?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
type="text"
|
||||
className="configure-button"
|
||||
icon={<ConfigureIcon />}
|
||||
data-testid="show-drawer"
|
||||
onClick={showDrawer}
|
||||
>
|
||||
Configure
|
||||
</Button>
|
||||
|
||||
<DrawerContainer
|
||||
title={drawerTitle}
|
||||
placement="right"
|
||||
width="50%"
|
||||
onClose={handleClose}
|
||||
open={visible}
|
||||
rootClassName="settings-container-root"
|
||||
>
|
||||
<OverlayScrollbar>
|
||||
<DashboardSettingsContent variableViewModeRef={variableViewModeRef} />
|
||||
</OverlayScrollbar>
|
||||
</DrawerContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SettingsDrawer;
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Button as ButtonComponent, Drawer } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled.div`
|
||||
margin-top: 0.5rem;
|
||||
`;
|
||||
|
||||
export const Button = styled(ButtonComponent)`
|
||||
&&& {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
|
||||
export const DrawerContainer = styled(Drawer)`
|
||||
.ant-drawer-header {
|
||||
padding: 16px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.ant-drawer-body {
|
||||
padding-top: 0;
|
||||
}
|
||||
`;
|
||||
@@ -17,7 +17,7 @@ import TimePreference from 'components/TimePreferenceDropDown';
|
||||
import { PANEL_TYPES, PanelDisplay } from 'constants/queryBuilder';
|
||||
import GraphTypes, {
|
||||
ItemsProps,
|
||||
} from 'container/NewDashboard/ComponentsSlider/menuItems';
|
||||
} from 'container/DashboardContainer/ComponentsSlider/menuItems';
|
||||
import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import {
|
||||
listViewInitialLogQuery,
|
||||
PANEL_TYPES_INITIAL_QUERY,
|
||||
} from 'container/NewDashboard/ComponentsSlider/constants';
|
||||
} from 'container/DashboardContainer/ComponentsSlider/constants';
|
||||
import {
|
||||
defaultLogsSelectedColumns,
|
||||
defaultTraceSelectedColumns,
|
||||
|
||||
@@ -158,6 +158,8 @@ function PublicDashboardContainer({
|
||||
modalSelectedInterval={selectedTimeRangeLabel as Time}
|
||||
disableUrlSync
|
||||
showRecentlyUsed={false}
|
||||
modalInitialStartTime={selectedTimeRange.startTime * 1000}
|
||||
modalInitialEndTime={selectedTimeRange.endTime * 1000}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import './ToolbarActions.styles.scss';
|
||||
import { Button } from 'antd';
|
||||
import { LogsExplorerShortcuts } from 'constants/shortcuts/logsExplorerShortcuts';
|
||||
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||
import { Play, X } from 'lucide-react';
|
||||
import { Loader2, Play } from 'lucide-react';
|
||||
import { MutableRefObject, useEffect } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
|
||||
@@ -37,37 +37,49 @@ export default function RightToolbarActions({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [onStageRunQuery, showLiveLogs]);
|
||||
|
||||
if (showLiveLogs) return <div />;
|
||||
if (showLiveLogs)
|
||||
return (
|
||||
<div className="right-toolbar-actions-container">
|
||||
<Button
|
||||
type="primary"
|
||||
className="run-query-btn periscope-btn primary"
|
||||
disabled
|
||||
icon={<Play size={14} />}
|
||||
>
|
||||
Run Query
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
const handleCancelQuery = (): void => {
|
||||
if (listQueryKeyRef?.current) {
|
||||
queryClient.cancelQueries(listQueryKeyRef.current);
|
||||
}
|
||||
if (chartQueryKeyRef?.current) {
|
||||
queryClient.cancelQueries(chartQueryKeyRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="right-toolbar-actions-container">
|
||||
{isLoadingQueries ? (
|
||||
<div className="loading-container">
|
||||
<Button className="loading-btn" loading={isLoadingQueries} />
|
||||
<Button
|
||||
icon={<X size={14} />}
|
||||
className="cancel-run"
|
||||
onClick={(): void => {
|
||||
if (listQueryKeyRef?.current) {
|
||||
queryClient.cancelQueries(listQueryKeyRef.current);
|
||||
}
|
||||
if (chartQueryKeyRef?.current) {
|
||||
queryClient.cancelQueries(chartQueryKeyRef.current);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Cancel Run
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
type="default"
|
||||
icon={<Loader2 size={14} className="loading-icon animate-spin" />}
|
||||
className="cancel-query-btn periscope-btn danger"
|
||||
onClick={handleCancelQuery}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
className="right-toolbar"
|
||||
className="run-query-btn periscope-btn primary"
|
||||
disabled={isLoadingQueries}
|
||||
onClick={onStageRunQuery}
|
||||
icon={<Play size={14} />}
|
||||
>
|
||||
Stage & Run Query
|
||||
Run Query
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -97,9 +97,9 @@ describe('ToolbarActions', () => {
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
|
||||
const stageNRunBtn = queryByText('Stage & Run Query');
|
||||
expect(stageNRunBtn).toBeInTheDocument();
|
||||
await userEvent.click(stageNRunBtn as HTMLElement);
|
||||
const runQueryBtn = queryByText('Run Query');
|
||||
expect(runQueryBtn).toBeInTheDocument();
|
||||
await userEvent.click(runQueryBtn as HTMLElement);
|
||||
expect(onStageRunQuery).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
// import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import useDashboardVariableUpdate from '../../NewDashboard/DashboardVariablesSelection/useDashboardVariableUpdate';
|
||||
import useDashboardVariableUpdate from '../../DashboardContainer/DashboardVariablesSelection/useDashboardVariableUpdate';
|
||||
import { getAggregateColumnHeader } from './drilldownUtils';
|
||||
import { AggregateData } from './useAggregateDrilldown';
|
||||
|
||||
|
||||
@@ -14,6 +14,26 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.right-toolbar-actions-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.cancel-query-btn {
|
||||
min-width: 96px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.run-query-btn {
|
||||
min-width: 96px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.timeRange {
|
||||
|
||||
@@ -366,6 +366,8 @@ function DateTimeSelection({
|
||||
)}
|
||||
data-testid="dropDown"
|
||||
items={options}
|
||||
minTime={minTime}
|
||||
maxTime={maxTime}
|
||||
/>
|
||||
|
||||
<FormItem hidden={refreshButtonHidden}>
|
||||
|
||||
@@ -139,7 +139,22 @@
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
letter-spacing: 0.14px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
|
||||
.time-value {
|
||||
font-size: 11px;
|
||||
color: var(--bg-vanilla-400);
|
||||
background-color: var(--bg-ink-200);
|
||||
padding: 4px 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.date-time-options-btn:hover {
|
||||
&.ant-btn-text {
|
||||
background-color: rgba(171, 189, 255, 0.04) !important;
|
||||
@@ -245,6 +260,99 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
|
||||
.input-error-message-container {
|
||||
color: var(--bg-cherry-400);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
cursor: default;
|
||||
font-size: 12px;
|
||||
gap: 8px;
|
||||
background-color: rgba(255, 219, 218, 0.1);
|
||||
border-radius: 3px;
|
||||
|
||||
.input-error-message-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-error-message-description {
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-container {
|
||||
width: 360px;
|
||||
height: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.calendar-container-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 6px 24px;
|
||||
width: 100%;
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
letter-spacing: 0.14px;
|
||||
line-height: 19px;
|
||||
border-bottom: 1px solid var(--bg-slate-400);
|
||||
}
|
||||
|
||||
.calendar-container-body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.periscope-calendar {
|
||||
border-radius: 4px;
|
||||
border: none !important;
|
||||
background: none !important;
|
||||
padding: 0px !important;
|
||||
padding-top: 8px !important;
|
||||
}
|
||||
|
||||
.periscope-calendar-day {
|
||||
button {
|
||||
&:hover {
|
||||
background-color: var(--bg-robin-500) !important;
|
||||
color: var(--bg-vanilla-100) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-actions {
|
||||
width: 100%;
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
.apply-btn,
|
||||
.cancel-btn {
|
||||
width: 80px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.recently-used-container {
|
||||
@@ -335,6 +443,11 @@
|
||||
|
||||
.date-time-options-btn {
|
||||
color: var(--bg-slate-400);
|
||||
|
||||
.time-value {
|
||||
background-color: var(--bg-vanilla-300);
|
||||
color: var(--bg-slate-400);
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
@@ -432,6 +545,17 @@
|
||||
background: var(--bg-vanilla-100);
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
.calendar-container {
|
||||
.periscope-calendar-day {
|
||||
button {
|
||||
&:hover {
|
||||
background-color: var(--bg-robin-500) !important;
|
||||
color: var(--bg-ink-500) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1400px) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Tooltip } from 'antd';
|
||||
import { Clock } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { RefreshTextContainer, Typography } from './styles';
|
||||
import { RefreshTextContainer } from './styles';
|
||||
|
||||
function RefreshText({
|
||||
onLastRefreshHandler,
|
||||
@@ -23,7 +25,9 @@ function RefreshText({
|
||||
|
||||
return (
|
||||
<RefreshTextContainer refreshButtonHidden={refreshButtonHidden}>
|
||||
<Typography>{refreshText}</Typography>
|
||||
<Tooltip title={refreshText}>
|
||||
<Clock size={12} />
|
||||
</Tooltip>
|
||||
</RefreshTextContainer>
|
||||
);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user