Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ interface RefreshIntervalPickerProps {
value?: DurationString;
onChange: (value: DurationString) => void;
height?: string;
disabled?: boolean;
}

export function RefreshIntervalPicker(props: RefreshIntervalPickerProps): ReactElement {
const { value, onChange, timeOptions, height } = props;
const { value, onChange, timeOptions, height, disabled = false } = props;

// If the dashboard refresh interval is not provided in timeOptions, it will create a specific option for the select
const customInterval = useMemo(() => {
Expand All @@ -39,6 +40,7 @@ export function RefreshIntervalPicker(props: RefreshIntervalPickerProps): ReactE
<Select
id="refreshInterval"
value={value}
disabled={disabled}
onChange={(event) => {
const duration = event.target.value as DurationString;
onChange(duration);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ import { DurationString } from '@perses-dev/spec';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React, { ReactElement } from 'react';
import { SnackbarProvider } from '@perses-dev/components';
import { TimeRangeProviderBasic, TimeRangeProviderWithQueryParams } from '@perses-dev/plugin-system';
import {
TimeRangeProviderBasic,
TimeRangeProviderWithQueryParams,
TimeRangeSettingsProvider,
} from '@perses-dev/plugin-system';
import { MemoryRouter } from 'react-router-dom';
import { QueryParamProvider } from 'use-query-params';
import { ReactRouter6Adapter } from 'use-query-params/adapters/react-router-6';
Expand Down Expand Up @@ -53,9 +57,9 @@ describe('TimeRangeControls', () => {
return <TimeRangeControls timeZone={timeZone} onTimeZoneChange={(tz) => setTimeZone(tz.value)} />;
};

const renderTimeRangeControls = (testURLParams: boolean): void => {
const renderTimeRangeControls = (testURLParams: boolean, disableAutoRefresh = false): void => {
renderWithContext(
<>
<TimeRangeSettingsProvider disableAutoRefresh={disableAutoRefresh}>
{testURLParams ? (
<TimeRangeProviderWithQueryParams
initialRefreshInterval={testDefaultRefreshInterval}
Expand All @@ -71,7 +75,7 @@ describe('TimeRangeControls', () => {
<ControlsWithTZ />
</TimeRangeProviderBasic>
)}
</>,
</TimeRangeSettingsProvider>,
undefined
);
};
Expand All @@ -86,5 +90,12 @@ describe('TimeRangeControls', () => {
expect(dateButton).toHaveTextContent(/5 minutes/i);
});

it('should disable the refresh interval picker when disableAutoRefresh is enabled', () => {
renderTimeRangeControls(false, true);
const refreshIntervalPicker = screen.getByLabelText(/Select refresh interval/i);
expect(refreshIntervalPicker).toHaveAttribute('aria-disabled', 'true');
expect(screen.getByText('Off')).toBeInTheDocument();
});

// TODO: add additional tests for absolute time selection, other inputs, form validation, etc.
});
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
useShowCustomTimeRangeSetting,
useTimeRangeOptionsSetting,
useShowZoomRangeSetting,
useDisableAutoRefreshSetting,
} from '../../runtime';
export const DEFAULT_REFRESH_INTERVAL_OPTIONS: TimeOption[] = [
{ value: { pastDuration: '0s' }, display: 'Off' },
Expand All @@ -46,6 +47,7 @@ export const DEFAULT_REFRESH_INTERVAL_OPTIONS: TimeOption[] = [
];

const DEFAULT_HEIGHT = '34px';
const DISABLED_REFRESH_INTERVAL: DurationString = '0s';

interface TimeRangeControlsProps {
// The controls look best at heights >= 28 pixels
Expand All @@ -55,6 +57,7 @@ interface TimeRangeControlsProps {
showRefreshInterval?: boolean;
showCustomTimeRange?: boolean;
showZoomButtons?: boolean;
disableAutoRefresh?: boolean;
timePresets?: TimeOption[];
timeZone: string;
onTimeZoneChange: (timeZone: TimeZoneOption) => void;
Expand All @@ -67,6 +70,7 @@ export function TimeRangeControls({
showRefreshInterval = true,
showCustomTimeRange,
showZoomButtons = true,
disableAutoRefresh,
timePresets,
timeZone,
onTimeZoneChange,
Expand All @@ -75,6 +79,7 @@ export function TimeRangeControls({

const showCustomTimeRangeValue = useShowCustomTimeRangeSetting(showCustomTimeRange);
const showZoomInOutButtons = useShowZoomRangeSetting(showZoomButtons);
const isAutoRefreshDisabled = useDisableAutoRefreshSetting(disableAutoRefresh);
const timePresetsValue = useTimeRangeOptionsSetting(timePresets);

// Convert height to a string, then use the string for styling
Expand All @@ -91,9 +96,12 @@ export function TimeRangeControls({
// set the new refresh interval both in the dashboard context & as query param
const handleRefreshIntervalChange = useCallback(
(duration: DurationString) => {
if (isAutoRefreshDisabled) {
return;
}
Comment on lines +99 to +101

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check is redundant. The picker is disabled when isAutoRefreshDisabled is true
(line 239), so onChange never fires and this handler is never called. So, this check is safe to be removed.

setRefreshInterval(duration);
},
[setRefreshInterval]
[isAutoRefreshDisabled, setRefreshInterval]
);

const fromDurationToMillis = (strDuration: string): number => {
Expand Down Expand Up @@ -207,21 +215,28 @@ export function TimeRangeControls({
</InfoTooltip>
)}
{showRefreshInterval && (
<InfoTooltip description={TOOLTIP_TEXT.refreshInterval}>
<InfoTooltip
description={
isAutoRefreshDisabled ? TOOLTIP_TEXT.refreshIntervalDisabledByAdmin : TOOLTIP_TEXT.refreshInterval
}
>
<RefreshIntervalPicker
timeOptions={DEFAULT_REFRESH_INTERVAL_OPTIONS}
value={
/* TODO: There is a bug here which should be fixed in a proper way. (This is only a quick remedy)
display: 1m has the pastDuration of 60s. Initially (if the persisted value is 1m) when the page is loaded, instead of 60s, 1m is passed down.
isAutoRefreshDisabled
? DISABLED_REFRESH_INTERVAL
: /* TODO: There is a bug here which should be fixed in a proper way. (This is only a quick remedy)
display: 1m has the pastDuration of 60s. Initially (if the persisted value is 1m) when the page is loaded, instead of 60s, 1m is passed down.
This only happens for 1m, because for other items the display and the pastDuration are the same. Example 30s-30s
HERE The value MUST always be pastDuration, otherwise the component would not work as expected.
HERE The value MUST always be pastDuration, otherwise the component would not work as expected.
*/
DEFAULT_REFRESH_INTERVAL_OPTIONS.some((i) => i.value.pastDuration === refreshInterval)
? refreshInterval
: DEFAULT_REFRESH_INTERVAL_OPTIONS.find((i) => i.display === refreshInterval)?.value.pastDuration
DEFAULT_REFRESH_INTERVAL_OPTIONS.some((i) => i.value.pastDuration === refreshInterval)
? refreshInterval
: DEFAULT_REFRESH_INTERVAL_OPTIONS.find((i) => i.display === refreshInterval)?.value.pastDuration
}
onChange={handleRefreshIntervalChange}
height={height}
disabled={isAutoRefreshDisabled}
/>
</InfoTooltip>
)}
Expand Down
1 change: 1 addition & 0 deletions plugin-system/src/constants/user-interface-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const TOOLTIP_TEXT = {
// Time range controls buttons
refresh: 'Refresh',
refreshInterval: 'Auto refresh interval',
refreshIntervalDisabledByAdmin: 'Auto-refresh has been turned off by your administrator.',
zoomIn: 'Zoom in',
zoomOut: 'Zoom out',
};
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
} from '@perses-dev/spec';
import { useQueryClient } from '@tanstack/react-query';
import { getRefreshIntervalInMs } from './refresh-interval';
import { useDisableAutoRefreshSetting } from './TimeRangeSettingsProvider';

export interface TimeRangeProviderProps {
timeRange: TimeRangeValue;
Expand Down Expand Up @@ -72,6 +73,7 @@ export function useSuggestedStepMs(width?: number): number {
*/
export function TimeRangeProvider(props: TimeRangeProviderProps): ReactElement {
const { timeRange, refreshInterval, children, setTimeRange, setRefreshInterval } = props;
const disableAutoRefresh = useDisableAutoRefreshSetting();

const queryClient = useQueryClient();
const [absoluteTimeRange, setAbsoluteTimeRange] = useState<AbsoluteTimeRange>(
Expand All @@ -86,6 +88,17 @@ export function TimeRangeProvider(props: TimeRangeProviderProps): ReactElement {
[setTimeRange]
);

// When auto-refresh is disabled by admin, leave URL/spec values untouched and no-op changes.
const handleSetRefreshInterval = useCallback(
(value: DurationString) => {
if (disableAutoRefresh) {
return;
}
setRefreshInterval(value);
},
[disableAutoRefresh, setRefreshInterval]
);

// Refresh is called when clicking on the refresh button, it refreshes all queries including variables
const refresh = useCallback(() => {
setAbsoluteTimeRange(isRelativeTimeRange(timeRange) ? toAbsoluteTimeRange(timeRange) : timeRange);
Expand All @@ -106,7 +119,11 @@ export function TimeRangeProvider(props: TimeRangeProviderProps): ReactElement {
});
}, [queryClient, timeRange]);

const refreshIntervalInMs = useMemo(() => getRefreshIntervalInMs(refreshInterval), [refreshInterval]);
// Gate the timer only — do not rewrite refreshInterval / ?refresh= so re-enabling restores them.
const refreshIntervalInMs = useMemo(
() => (disableAutoRefresh ? 0 : getRefreshIntervalInMs(refreshInterval)),
[disableAutoRefresh, refreshInterval]
);
useEffect(() => {
if (refreshIntervalInMs > 0) {
const interval = setInterval(() => {
Expand All @@ -125,15 +142,15 @@ export function TimeRangeProvider(props: TimeRangeProviderProps): ReactElement {
refresh,
refreshInterval: refreshInterval,
refreshIntervalInMs: refreshIntervalInMs,
setRefreshInterval: setRefreshInterval,
setRefreshInterval: handleSetRefreshInterval,
};
}, [
absoluteTimeRange,
handleSetTimeRange,
refresh,
refreshInterval,
refreshIntervalInMs,
setRefreshInterval,
handleSetRefreshInterval,
timeRange,
]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,22 @@ const DEFAULT_OPTIONS: DurationString[] = ['5m', '15m', '30m', '1h', '6h', '12h'
const defaultTimeRangeSettings: TimeRangeSettings = {
showCustom: true,
showZoomButtons: true,
disableAutoRefresh: false,
options: DEFAULT_OPTIONS.map((duration) => buildRelativeTimeOption(duration)),
};

export interface TimeRangeSettingsProviderProps {
showCustom?: boolean;
showZoomButtons?: boolean;
disableAutoRefresh?: boolean;
options?: TimeOption[];
children: ReactNode;
}

export interface TimeRangeSettings {
showCustom: boolean;
showZoomButtons: boolean;
disableAutoRefresh: boolean;
options: TimeOption[];
}

Expand Down Expand Up @@ -88,6 +91,18 @@ export function useTimeRangeOptionsSetting(override?: TimeOption[]): TimeOption[
return showCustomTimeRange;
}

/**
* Get the current value of the disableAutoRefresh setting.
* @param override If set, the value of the provider will be overridden by this value.
*/
export function useDisableAutoRefreshSetting(override?: boolean): boolean {
const disableAutoRefresh = useTimeRangeSettings().disableAutoRefresh;
if (override !== undefined) {
return override;
}
return disableAutoRefresh;
}

/**
* Provider implementation that supplies the time range state at runtime.
*/
Expand All @@ -97,9 +112,11 @@ export function TimeRangeSettingsProvider(props: TimeRangeSettingsProviderProps)
showCustom: props.showCustom === undefined ? defaultTimeRangeSettings.showCustom : props.showCustom,
showZoomButtons:
props.showZoomButtons === undefined ? defaultTimeRangeSettings.showZoomButtons : props.showZoomButtons,
disableAutoRefresh:
props.disableAutoRefresh === undefined ? defaultTimeRangeSettings.disableAutoRefresh : props.disableAutoRefresh,
options: props.options === undefined ? defaultTimeRangeSettings.options : props.options,
};
}, [props.showCustom, props.showZoomButtons, props.options]);
}, [props.showCustom, props.showZoomButtons, props.disableAutoRefresh, props.options]);

return <TimeRangeSettingsContext.Provider value={ctx}>{props.children}</TimeRangeSettingsContext.Provider>;
}
Loading