Compare commits

...

21 Commits

Author SHA1 Message Date
Abhishek Kumar Singh
97e1be83f9 Merge branch 'main' into chore/remove_eval_delay_from_safe_rules 2026-01-15 14:52:46 +05:30
Abhishek Kumar Singh
b53b40121d fix: removed increase time agg from safe aggregations 2026-01-15 14:52:28 +05:30
Abhi kumar
e75abcb108 Fix/animation offload to cpu (#9961)
* fix: transition is getting offloaded to cpu because gpu can't handle it in composition

* chore: changed transition from all to transform only
2026-01-15 13:43:44 +05:30
primus-bot[bot]
ffa15f9ec1 chore(release): bump to v0.107.0 (#10007)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2026-01-15 12:18:52 +05:30
Ishan
87562f5387 feat: onGroupBy - show timeseries panel - #3403 (#9921)
* feat: onGroupBy - show timeseries

* feat: updated testcases for tableview actions

* feat: updated pipeline file to remove groupBy as not used

* feat: updated better code checks

* feat: updated better code checks

* feat: updated tableview actions to update query

* feat: updated types and test cases

* feat: updated dataType (noramlized)
2026-01-15 08:53:08 +05:30
Ishan
11a7512a25 feat: trace id pinned (#9981) 2026-01-13 19:59:14 +05:30
Yunus M
f925a15f30 feat: improve date range selection ux (#9994)
* feat: improve date range selection ux
2026-01-13 13:55:57 +00:00
Ashwin Bhatkal
a3fe2c2589 chore: rename Variables folder to DashboardVariableSettings (#9990)
* chore: rename Variables folder to DashboardVariableSettings

* fix: fix tests
2026-01-13 11:25:42 +00:00
Aditya Singh
2719c9b6a7 Fix: Make exists in small case recognise as valid non value op (#9989)
* fix: make exists in small case recognise as valid non value op

* fix: lint fix
2026-01-13 10:56:38 +00:00
Ashwin Bhatkal
b4282be3ac chore: make settings drawer reusable (#9985)
* chore: make settings drawer reusable

* chore: clean up styles as well

* fix: resolve comments

* chore: trigger build

* chore: resolve comments
2026-01-13 09:33:05 +00:00
Ashwin Bhatkal
15fe3a7163 fix: close button when importing dashboard json (#9987)
* fix: close button when importing dashboard json

* fix: remove x icon
2026-01-13 05:59:11 +00:00
Amlan Kumar Nandy
30b98582b4 chore: fix undefined labels error in alerts (#9589)
* chore: fix undefined labels error in alerts

* chore: fix CI

* chore: minor fix

* chore: add tests

* chore: additional changes

* chore: additonal cleanup

* chore: update tests

* chore: update mock

* chore: update checks

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2026-01-13 05:27:16 +00:00
Vikrant Gupta
99b9e27bca fix(dashboard): delete dashboard shouldn't break on license check (#9984)
* fix(dashboard): delete dashboard shouldn't break on license check

* fix(dashboard): add integration tests

* fix(dashboard): add integration tests

* fix(dashboard): remove license check from public dashboard delete

* fix(dashboard): update delete public
2026-01-12 23:40:54 +05:30
Yunus M
7e72f501a7 feat: Improved UX for Date Time Picker (#9944)
* feat: enable inline edit for selected time

* feat: handle absolute and relative time formats

* feat: support epoch timeranges

* feat: match styles for datetime shorthand pill

* feat: handle time range error flows

* feat: hide last refreshed for custom time ranges

* feat: cleanup

* feat: use calendar range for custom date time picker

* fix: update tests to check for run query rather than stage & run query

* feat: pass modalStartTime & modalEndTime in cases where isModalSelection is true

* feat: pass minTime & maxTime for CustomPicker in topnav component

* fix: add safety check for selected time

* feat: show recently use custom time ranges

* fix: cancel query button light mode text color

* feat: move calendar content to a container

* feat: address review comments
2026-01-12 21:18:58 +05:30
Ashwin Bhatkal
97d7b81a73 chore: rename NewDashboard to DashboardContainer (#9980)
* chore: rename NewDashboard to DashboardContainer

* chore: resolve comments and fix tests
2026-01-12 15:25:43 +05:30
Abhishek Kumar Singh
70cd4a6e58 fix: openapi ci fail 2026-01-08 13:33:34 +05:30
Abhishek Kumar Singh
4af551b483 Merge branch 'main' into chore/remove_eval_delay_from_safe_rules 2026-01-08 11:59:33 +05:30
Abhishek Kumar Singh
ceee42616a refactor: added handling for space agg when calculating the eval delay 2026-01-07 14:37:32 +05:30
Abhishek Kumar Singh
e10c1b9723 feat: added handling for log and traces based aggregation for QB in CalculateEvalDelay 2025-12-18 13:43:25 +05:30
Abhishek Kumar Singh
06ae762070 fix: use matchType and compare operator from threshold rather than ruleCondition 2025-12-17 21:03:54 +05:30
Abhishek Kumar Singh
3cc34baf74 chore: remove eval delay for safe eval rules 2025-12-17 20:39:32 +05:30
158 changed files with 3690 additions and 952 deletions

17
.github/CODEOWNERS vendored
View File

@@ -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

View File

@@ -176,7 +176,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.106.0
image: signoz/signoz:v0.107.0
command:
- --config=/root/config/prometheus.yml
ports:

View File

@@ -117,7 +117,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.106.0
image: signoz/signoz:v0.107.0
command:
- --config=/root/config/prometheus.yml
ports:

View File

@@ -179,7 +179,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.106.0}
image: signoz/signoz:${VERSION:-v0.107.0}
container_name: signoz
command:
- --config=/root/config/prometheus.yml

View File

@@ -111,7 +111,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.106.0}
image: signoz/signoz:${VERSION:-v0.107.0}
container_name: signoz
command:
- --config=/root/config/prometheus.yml

View File

@@ -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
}

View File

@@ -26,6 +26,10 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
if err != nil {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "evaluation is invalid: %v", err)
}
// calculate eval delay based on rule config
evalDelay := baserules.CalculateEvalDelay(opts.Rule, opts.ManagerOpts.EvalDelay)
if opts.Rule.RuleType == ruletypes.RuleTypeThreshold {
// create a threshold rule
tr, err := baserules.NewThresholdRule(
@@ -35,7 +39,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
opts.Reader,
opts.Querier,
opts.SLogger,
baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay),
baserules.WithEvalDelay(evalDelay),
baserules.WithSQLStore(opts.SQLStore),
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
baserules.WithMetadataStore(opts.ManagerOpts.MetadataStore),
@@ -84,7 +88,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
opts.Querier,
opts.SLogger,
opts.Cache,
baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay),
baserules.WithEvalDelay(evalDelay),
baserules.WithSQLStore(opts.SQLStore),
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
baserules.WithMetadataStore(opts.ManagerOpts.MetadataStore),

View 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;

View File

@@ -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%);

View File

@@ -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>
);
}

View File

@@ -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">

View File

@@ -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;
}
}
}
}

View File

@@ -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;

View File

@@ -176,6 +176,8 @@ function HostMetricTraces({
onTimeChange={handleTimeChange}
defaultRelativeTime="5m"
modalSelectedInterval={selectedInterval}
modalInitialStartTime={timeRange.startTime * 1000}
modalInitialEndTime={timeRange.endTime * 1000}
/>
</div>
</div>

View File

@@ -87,6 +87,8 @@ function HostMetricLogsDetailedView({
onTimeChange={handleTimeChange}
defaultRelativeTime="5m"
modalSelectedInterval={selectedInterval}
modalInitialStartTime={timeRange.startTime * 1000}
modalInitialEndTime={timeRange.endTime * 1000}
/>
</div>
</div>

View File

@@ -211,6 +211,8 @@ function Metrics({
defaultRelativeTime="5m"
isModalTimeSelection={isModalTimeSelection}
modalSelectedInterval={selectedInterval}
modalInitialStartTime={timeRange.startTime * 1000}
modalInitialEndTime={timeRange.endTime * 1000}
/>
</div>
</div>

View File

@@ -1,16 +1,16 @@
import { DrawerProps } from 'antd';
import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { ActionItemProps } from 'container/LogDetailedView/ActionItem';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { VIEWS } from './constants';
export type LogDetailProps = {
log: ILog | null;
selectedTab: VIEWS;
onGroupByAttribute?: (fieldKey: string, dataType?: DataTypes) => Promise<void>;
handleChangeSelectedView?: ChangeViewFunctionType;
isListViewPanel?: boolean;
listViewPanelSelectedFields?: IField[] | null;
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &

View File

@@ -55,11 +55,11 @@ function LogDetailInner({
log,
onClose,
onAddToQuery,
onGroupByAttribute,
onClickActionItem,
selectedTab,
isListViewPanel = false,
listViewPanelSelectedFields,
handleChangeSelectedView,
}: LogDetailInnerProps): JSX.Element {
const initialContextQuery = useInitialQuery(log);
const [contextQuery, setContextQuery] = useState<Query | undefined>(
@@ -365,10 +365,10 @@ function LogDetailInner({
logData={log}
onAddToQuery={onAddToQuery}
onClickActionItem={onClickActionItem}
onGroupByAttribute={onGroupByAttribute}
isListViewPanel={isListViewPanel}
selectedOptions={options}
listViewPanelSelectedFields={listViewPanelSelectedFields}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
{selectedView === VIEW_TYPES.JSON && <JSONView logData={log} />}

View File

@@ -6,6 +6,7 @@ import cx from 'classnames';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { getSanitizedLogBody } from 'container/LogDetailedView/utils';
import { FontSize } from 'container/OptionsMenu/types';
import { useActiveLog } from 'hooks/logs/useActiveLog';
@@ -108,6 +109,7 @@ type ListLogViewProps = {
activeLog?: ILog | null;
linesPerRow: number;
fontSize: FontSize;
handleChangeSelectedView?: ChangeViewFunctionType;
};
function ListLogView({
@@ -118,6 +120,7 @@ function ListLogView({
activeLog,
linesPerRow,
fontSize,
handleChangeSelectedView,
}: ListLogViewProps): JSX.Element {
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
@@ -131,7 +134,6 @@ function ListLogView({
onAddToQuery: handleAddToQuery,
onSetActiveLog: handleSetActiveContextLog,
onClearActiveLog: handleClearActiveContextLog,
onGroupByAttribute,
} = useActiveLog();
const isDarkMode = useIsDarkMode();
@@ -255,7 +257,7 @@ function ListLogView({
onAddToQuery={handleAddToQuery}
selectedTab={VIEW_TYPES.CONTEXT}
onClose={handlerClearActiveContextLog}
onGroupByAttribute={onGroupByAttribute}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
</>
@@ -264,6 +266,7 @@ function ListLogView({
ListLogView.defaultProps = {
activeLog: null,
handleChangeSelectedView: undefined,
};
LogGeneralField.defaultProps = {

View File

@@ -39,6 +39,7 @@ function RawLogView({
selectedFields = [],
fontSize,
onLogClick,
handleChangeSelectedView,
}: RawLogViewProps): JSX.Element {
const {
isHighlighted: isUrlHighlighted,
@@ -52,7 +53,6 @@ function RawLogView({
onSetActiveLog,
onClearActiveLog,
onAddToQuery,
onGroupByAttribute,
} = useActiveLog();
const [selectedTab, setSelectedTab] = useState<VIEWS | undefined>();
@@ -224,13 +224,12 @@ function RawLogView({
onClose={handleCloseLogDetail}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
onGroupByAttribute={onGroupByAttribute}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
</RawLogViewContainer>
);
}
RawLogView.defaultProps = {
isActiveLog: false,
isReadOnly: false,

View File

@@ -1,3 +1,4 @@
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { FontSize } from 'container/OptionsMenu/types';
import { MouseEvent } from 'react';
import { IField } from 'types/api/logs/fields';
@@ -14,6 +15,7 @@ export interface RawLogViewProps {
fontSize: FontSize;
selectedFields?: IField[];
onLogClick?: (log: ILog, event: MouseEvent) => void;
handleChangeSelectedView?: ChangeViewFunctionType;
}
export interface RawLogContentProps {

View File

@@ -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', () => ({

View File

@@ -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';

View File

@@ -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);

View File

@@ -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);
}
}
}
}
}
}

View File

@@ -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);

View File

@@ -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

View File

@@ -0,0 +1,7 @@
import { MutableRefObject } from 'react';
export interface VariablesSettingsTab {
resetState: () => void;
}
export type VariablesSettingsTabHandle = MutableRefObject<VariablesSettingsTab | null>;

View File

@@ -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;

View File

@@ -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';

View File

@@ -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;

View File

@@ -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();

View File

@@ -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');

View File

@@ -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">

View File

@@ -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';

View File

@@ -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';

View File

@@ -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', () => ({

View File

@@ -266,6 +266,8 @@ export default function Events({
onTimeChange={handleTimeChange}
defaultRelativeTime="5m"
modalSelectedInterval={selectedInterval}
modalInitialStartTime={timeRange.startTime * 1000}
modalInitialEndTime={timeRange.endTime * 1000}
/>
</div>
</div>

View File

@@ -93,6 +93,8 @@ function EntityLogsDetailedView({
onTimeChange={handleTimeChange}
defaultRelativeTime="5m"
modalSelectedInterval={selectedInterval}
modalInitialStartTime={timeRange.startTime * 1000}
modalInitialEndTime={timeRange.endTime * 1000}
/>
</div>
</div>

View File

@@ -258,6 +258,8 @@ function EntityMetrics<T>({
defaultRelativeTime="5m"
isModalTimeSelection={isModalTimeSelection}
modalSelectedInterval={selectedInterval}
modalInitialStartTime={timeRange.startTime * 1000}
modalInitialEndTime={timeRange.endTime * 1000}
/>
</div>
</div>

View File

@@ -188,6 +188,8 @@ function EntityTraces({
onTimeChange={handleTimeChange}
defaultRelativeTime="5m"
modalSelectedInterval={selectedInterval}
modalInitialStartTime={timeRange.startTime * 1000}
modalInitialEndTime={timeRange.endTime * 1000}
/>
</div>
</div>

View File

@@ -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({

View File

@@ -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';

View File

@@ -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

View File

@@ -4,6 +4,7 @@ import { Switch, Typography } from 'antd';
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
import { MAX_LOGS_LIST_SIZE } from 'constants/liveTail';
import { LOCALSTORAGE } from 'constants/localStorage';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import GoToTop from 'container/GoToTop';
import { useOptionsMenu } from 'container/OptionsMenu';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
@@ -21,7 +22,13 @@ import { ILiveLogsLog } from '../LiveLogsList/types';
import LiveLogsListChart from '../LiveLogsListChart';
import { QueryHistoryState } from '../types';
function LiveLogsContainer(): JSX.Element {
interface LiveLogsContainerProps {
handleChangeSelectedView?: ChangeViewFunctionType;
}
function LiveLogsContainer({
handleChangeSelectedView,
}: LiveLogsContainerProps): JSX.Element {
const location = useLocation();
const [logs, setLogs] = useState<ILiveLogsLog[]>([]);
const { currentQuery, stagedQuery } = useQueryBuilder();
@@ -247,6 +254,7 @@ function LiveLogsContainer(): JSX.Element {
<LiveLogsList
logs={logs}
isLoading={initialLoading && logs.length === 0}
handleChangeSelectedView={handleChangeSelectedView}
/>
</div>
</div>
@@ -256,4 +264,8 @@ function LiveLogsContainer(): JSX.Element {
);
}
LiveLogsContainer.defaultProps = {
handleChangeSelectedView: undefined,
};
export default LiveLogsContainer;

View File

@@ -25,7 +25,11 @@ import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { LiveLogsListProps } from './types';
function LiveLogsList({ logs, isLoading }: LiveLogsListProps): JSX.Element {
function LiveLogsList({
logs,
isLoading,
handleChangeSelectedView,
}: LiveLogsListProps): JSX.Element {
const ref = useRef<VirtuosoHandle>(null);
const { isConnectionLoading } = useEventSource();
@@ -36,7 +40,6 @@ function LiveLogsList({ logs, isLoading }: LiveLogsListProps): JSX.Element {
activeLog,
onClearActiveLog,
onAddToQuery,
onGroupByAttribute,
onSetActiveLog,
} = useActiveLog();
@@ -72,6 +75,7 @@ function LiveLogsList({ logs, isLoading }: LiveLogsListProps): JSX.Element {
linesPerRow={options.maxLines}
selectedFields={selectedFields}
fontSize={options.fontSize}
handleChangeSelectedView={handleChangeSelectedView}
/>
);
}
@@ -85,10 +89,12 @@ function LiveLogsList({ logs, isLoading }: LiveLogsListProps): JSX.Element {
onAddToQuery={onAddToQuery}
onSetActiveLog={onSetActiveLog}
fontSize={options.fontSize}
handleChangeSelectedView={handleChangeSelectedView}
/>
);
},
[
handleChangeSelectedView,
onAddToQuery,
onSetActiveLog,
options.fontSize,
@@ -147,6 +153,7 @@ function LiveLogsList({ logs, isLoading }: LiveLogsListProps): JSX.Element {
appendTo: 'end',
activeLogIndex,
}}
handleChangeSelectedView={handleChangeSelectedView}
/>
) : (
<Card style={{ width: '100%' }} bodyStyle={CARD_BODY_STYLE}>
@@ -170,12 +177,11 @@ function LiveLogsList({ logs, isLoading }: LiveLogsListProps): JSX.Element {
log={activeLog}
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onGroupByAttribute={onGroupByAttribute}
onClickActionItem={onAddToQuery}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
</div>
);
}
export default memo(LiveLogsList);

View File

@@ -1,3 +1,4 @@
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { ILog } from 'types/api/logs/log';
export interface ILiveLogsLog {
@@ -8,4 +9,5 @@ export interface ILiveLogsLog {
export type LiveLogsListProps = {
logs: ILiveLogsLog[];
isLoading: boolean;
handleChangeSelectedView?: ChangeViewFunctionType;
};

View File

@@ -12,13 +12,13 @@ import {
Typography,
} from 'antd';
import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { OptionsQuery } from 'container/OptionsMenu/types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { ChevronDown, ChevronRight, Search } from 'lucide-react';
import { ReactNode, useState } from 'react';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { ActionItemProps } from './ActionItem';
import TableView from './TableView';
@@ -29,7 +29,7 @@ interface OverviewProps {
isListViewPanel?: boolean;
selectedOptions: OptionsQuery;
listViewPanelSelectedFields?: IField[] | null;
onGroupByAttribute?: (fieldKey: string, dataType?: DataTypes) => Promise<void>;
handleChangeSelectedView?: ChangeViewFunctionType;
}
type Props = OverviewProps &
@@ -42,8 +42,8 @@ function Overview({
onClickActionItem,
isListViewPanel = false,
selectedOptions,
onGroupByAttribute,
listViewPanelSelectedFields,
handleChangeSelectedView,
}: Props): JSX.Element {
const [isWrapWord, setIsWrapWord] = useState<boolean>(true);
const [isSearchVisible, setIsSearchVisible] = useState<boolean>(false);
@@ -208,11 +208,11 @@ function Overview({
logData={logData}
onAddToQuery={onAddToQuery}
fieldSearchInput={fieldSearchInput}
onGroupByAttribute={onGroupByAttribute}
onClickActionItem={onClickActionItem}
isListViewPanel={isListViewPanel}
selectedOptions={selectedOptions}
listViewPanelSelectedFields={listViewPanelSelectedFields}
handleChangeSelectedView={handleChangeSelectedView}
/>
</>
),
@@ -227,7 +227,7 @@ function Overview({
Overview.defaultProps = {
isListViewPanel: false,
listViewPanelSelectedFields: null,
onGroupByAttribute: undefined,
handleChangeSelectedView: undefined,
};
export default Overview;

Some files were not shown because too many files have changed in this diff Show More