Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keybindings: support time range copy/paste #960

Merged
merged 10 commits into from
Dec 13, 2024
30 changes: 29 additions & 1 deletion src/Components/IndexScene/IndexScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ import { AdHocFilterWithLabels } from '../../services/scenes';
import { FilterOp } from '../../services/filterTypes';
import { ShowLogsButtonScene } from './ShowLogsButtonScene';
import { CustomVariableValueSelectors } from './CustomVariableValueSelectors';
import { setupKeyboardShortcuts } from '../../services/keyboardShortcuts';
import { getCopiedTimeRange, PasteTimeEvent, setupKeyboardShortcuts } from '../../services/keyboardShortcuts';

export const showLogsButtonSceneKey = 'showLogsButtonScene';
export interface AppliedPattern {
Expand Down Expand Up @@ -180,6 +180,7 @@ export class IndexScene extends SceneObjectBase<IndexSceneState> {
const timeRange = sceneGraph.getTimeRange(this);

this._subs.add(timeRange.subscribeToState(this.limitMaxInterval(timeRange)));
this._subs.add(this.subscribeToEvent(PasteTimeEvent, this.subscribeToPasteTimeEvent));

const clearKeyBindings = setupKeyboardShortcuts(this);

Expand Down Expand Up @@ -221,6 +222,33 @@ export class IndexScene extends SceneObjectBase<IndexSceneState> {
});
}

private subscribeToPasteTimeEvent = async () => {
const copiedRange = await getCopiedTimeRange();

if (!copiedRange.isError) {
const timeRange = sceneGraph.getTimeRange(this);
const to = typeof copiedRange.range.to === 'string' ? copiedRange.range.to : undefined;
const from = typeof copiedRange.range.from === 'string' ? copiedRange.range.from : undefined;

const newRange = rangeUtil.convertRawToRange(copiedRange.range);

if (timeRange && newRange) {
timeRange.setState({
value: newRange,
to,
from,
});
} else {
logger.error(new Error('Invalid time range from clipboard'), {
msg: 'Invalid time range from clipboard',
sceneTimeRange: typeof timeRange,
to: to ?? '',
from: from ?? '',
});
}
}
};
gtk-grafana marked this conversation as resolved.
Show resolved Hide resolved

/**
* If user selects a time range longer then the max configured interval, show toast and set the previous time range.
* @param timeRange
Expand Down
86 changes: 84 additions & 2 deletions src/services/keyboardShortcuts.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { IndexScene } from '../Components/IndexScene/IndexScene';
import { KeybindingSet } from './KeybindingSet';
import { getAppEvents, locationService } from '@grafana/runtime';
import { SetPanelAttentionEvent } from '@grafana/data';
import { sceneGraph, VizPanel } from '@grafana/scenes';
import { BusEventBase, BusEventWithPayload, RawTimeRange, SetPanelAttentionEvent } from '@grafana/data';
import { sceneGraph, SceneObject, VizPanel } from '@grafana/scenes';
import { getExploreLink } from '../Components/Panels/PanelMenu';
import { getTimePicker } from './scenes';
import { OptionsWithLegend } from '@grafana/ui';
import { hasProp, isObj, isString } from './narrowing';

const appEvents = getAppEvents();

Expand Down Expand Up @@ -63,6 +64,26 @@ export function setupKeyboardShortcuts(scene: IndexScene) {
}),
});

// Copy time range
keybindings.addBinding({
key: 't c',
onTrigger: () => {
const timeRange = sceneGraph.getTimeRange(scene);
setWindowGrafanaSceneContext(timeRange);
gtk-grafana marked this conversation as resolved.
Show resolved Hide resolved
appEvents.publish(new CopyTimeEvent());
},
});

// Paste time range
keybindings.addBinding({
key: 't v',
onTrigger: () => {
const event = new PasteTimeEvent({ updateUrl: false });
scene.publishEvent(event);
appEvents.publish(event);
},
});

// Refresh
keybindings.addBinding({
key: 'd r',
Expand Down Expand Up @@ -146,3 +167,64 @@ export function toggleVizPanelLegend(vizPanel: VizPanel): void {
function hasLegendOptions(optionsWithLegend: unknown): optionsWithLegend is OptionsWithLegend {
return optionsWithLegend != null && typeof optionsWithLegend === 'object' && 'legend' in optionsWithLegend;
}

// Copied from https://github.com/grafana/grafana/blob/main/public/app/types/events.ts
// @todo export from core grafana
export class CopyTimeEvent extends BusEventBase {
static type = 'copy-time';
}

// Copied from https://github.com/grafana/grafana/blob/main/public/app/types/events.ts
// @todo export from core grafana
interface PasteTimeEventPayload {
updateUrl?: boolean;
timeRange?: string;
}

// Copied from https://github.com/grafana/grafana/blob/main/public/app/types/events.ts
// @todo export from core grafana
export class PasteTimeEvent extends BusEventWithPayload<PasteTimeEventPayload> {
static type = 'paste-time';
}

/**
* @todo delete after https://github.com/grafana/scenes/pull/999 is available
* @param activeScene
*/
export function setWindowGrafanaSceneContext(activeScene: SceneObject) {
const prevScene = (window as any).__grafanaSceneContext;

(window as any).__grafanaSceneContext = activeScene;

return () => {
if ((window as any).__grafanaSceneContext === activeScene) {
(window as any).__grafanaSceneContext = prevScene;
}
};
}

// taken from /Users/galen/projects/grafana/grafana/public/app/core/utils/timePicker.ts
type CopiedTimeRangeResult = { range: RawTimeRange; isError: false } | { range: string; isError: true };

// modified to narrow types from clipboard
export async function getCopiedTimeRange(): Promise<CopiedTimeRangeResult> {
gtk-grafana marked this conversation as resolved.
Show resolved Hide resolved
const raw = await navigator.clipboard.readText();
let unknownRange;

try {
unknownRange = JSON.parse(raw);

const range = isObj(unknownRange) && hasProp(unknownRange, 'to') && hasProp(unknownRange, 'from') && unknownRange;
if (range) {
const to = isString(range.to);
const from = isString(range.from);
if (to && from) {
return { range: { to, from }, isError: false };
}
}

return { range: raw, isError: true };
} catch (e) {
return { range: raw, isError: true };
}
}
6 changes: 3 additions & 3 deletions src/services/narrowing.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { SelectedTableRow } from '../Components/Table/LogLineCellComponent';
import { LogsVisualizationType } from './store';
import { FieldValue, ParserType } from './variables';
const isObj = (o: unknown): o is object => typeof o === 'object' && o !== null;
export const isObj = (o: unknown): o is object => typeof o === 'object' && o !== null;

function hasProp<K extends PropertyKey>(data: object, prop: K): data is Record<K, unknown> {
export function hasProp<K extends PropertyKey>(data: object, prop: K): data is Record<K, unknown> {
return prop in data;
}

const isString = (s: unknown) => (typeof s === 'string' && s) || '';
export const isString = (s: unknown) => (typeof s === 'string' && s) || '';

export const isRecord = (obj: unknown): obj is Record<string, unknown> => typeof obj === 'object';

Expand Down
Loading