mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-11 17:19:06 +08:00
feat: added shortcuts page in the side nav (#4506)
* feat: added shortcuts page in the side nav * fix: update shortcuts for add to dashboard and alerts * fix: cmd+enter should stage and run query * chore: refactor the shortcuts utils * feat: support run query even when input is focussed * fix: dropdown visibility change * feat: add shortcuts for sideNav * feat: auto focus logs explorer search bar with hotkey * fix: update the shortcuts for sideNav and dependencies * fix: remove dashboard and alert shortcuts * fix: minor typo changes
This commit is contained in:
parent
0e331dd177
commit
3a20862d0c
@ -41,5 +41,6 @@
|
||||
"SUPPORT": "SigNoz | Support",
|
||||
"LOGS_SAVE_VIEWS": "SigNoz | Logs Save Views",
|
||||
"TRACES_SAVE_VIEWS": "SigNoz | Traces Save Views",
|
||||
"DEFAULT": "Open source Observability Platform | SigNoz"
|
||||
"DEFAULT": "Open source Observability Platform | SigNoz",
|
||||
"SHORTCUTS": "SigNoz | Shortcuts"
|
||||
}
|
||||
|
@ -182,3 +182,7 @@ export const WorkspaceBlocked = Loadable(
|
||||
() =>
|
||||
import(/* webpackChunkName: "WorkspaceLocked" */ 'pages/WorkspaceLocked'),
|
||||
);
|
||||
|
||||
export const ShortcutsPage = Loadable(
|
||||
() => import(/* webpackChunkName: "ShortcutsPage" */ 'pages/Shortcuts'),
|
||||
);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
import Shortcuts from 'pages/Shortcuts/Shortcuts';
|
||||
import WorkspaceBlocked from 'pages/WorkspaceLocked';
|
||||
import { RouteProps } from 'react-router-dom';
|
||||
|
||||
@ -319,6 +320,13 @@ const routes: AppRoutes[] = [
|
||||
isPrivate: true,
|
||||
key: 'WORKSPACE_LOCKED',
|
||||
},
|
||||
{
|
||||
path: ROUTES.SHORTCUTS,
|
||||
exact: true,
|
||||
component: Shortcuts,
|
||||
isPrivate: true,
|
||||
key: 'SHORTCUTS',
|
||||
},
|
||||
];
|
||||
|
||||
export const SUPPORT_ROUTE: AppRoutes = {
|
||||
|
@ -44,6 +44,7 @@ const ROUTES = {
|
||||
LOGS_SAVE_VIEWS: '/logs-save-views',
|
||||
TRACES_SAVE_VIEWS: '/traces-save-views',
|
||||
WORKSPACE_LOCKED: '/workspace-locked',
|
||||
SHORTCUTS: '/shortcuts',
|
||||
} as const;
|
||||
|
||||
export default ROUTES;
|
||||
|
@ -1,3 +1,19 @@
|
||||
export const GlobalShortcuts = {
|
||||
SidebarCollapse: '\\+meta',
|
||||
NavigateToServices: 's+shift',
|
||||
NavigateToTraces: 't+shift',
|
||||
NavigateToLogs: 'l+shift',
|
||||
NavigateToDashboards: 'd+shift',
|
||||
NavigateToAlerts: 'a+shift',
|
||||
NavigateToExceptions: 'e+shift',
|
||||
};
|
||||
|
||||
export const GlobalShortcutsDescription = {
|
||||
SidebarCollapse: 'Collpase the sidebar',
|
||||
NavigateToServices: 'Navigate to Services page',
|
||||
NavigateToTraces: 'Navigate to Traces page',
|
||||
NavigateToLogs: 'Navigate to logs page',
|
||||
NavigateToDashboards: 'Navigate to dashboards page',
|
||||
NavigateToAlerts: 'Navigate to alerts page',
|
||||
NavigateToExceptions: 'Navigate to Exceptions page',
|
||||
};
|
||||
|
@ -0,0 +1,9 @@
|
||||
export const LogsExplorerShortcuts = {
|
||||
StageAndRunQuery: 'enter+meta',
|
||||
FocusTheSearchBar: 's',
|
||||
};
|
||||
|
||||
export const LogsExplorerShortcutsDescription = {
|
||||
StageAndRunQuery: 'Stage and Run the current query',
|
||||
FocusTheSearchBar: 'Shift the focus to the filter bar',
|
||||
};
|
@ -1,7 +1,10 @@
|
||||
import './ToolbarActions.styles.scss';
|
||||
|
||||
import { Button } from 'antd';
|
||||
import { LogsExplorerShortcuts } from 'constants/shortcuts/logsExplorerShortcuts';
|
||||
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||
import { Play } from 'lucide-react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
interface RightToolbarActionsProps {
|
||||
onStageRunQuery: () => void;
|
||||
@ -10,6 +13,16 @@ interface RightToolbarActionsProps {
|
||||
export default function RightToolbarActions({
|
||||
onStageRunQuery,
|
||||
}: RightToolbarActionsProps): JSX.Element {
|
||||
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
||||
|
||||
useEffect(() => {
|
||||
registerShortcut(LogsExplorerShortcuts.StageAndRunQuery, onStageRunQuery);
|
||||
|
||||
return (): void => {
|
||||
deregisterShortcut(LogsExplorerShortcuts.StageAndRunQuery);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [onStageRunQuery]);
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
|
@ -2,12 +2,16 @@ import './QueryBuilderSearch.styles.scss';
|
||||
|
||||
import { Select, Spin, Tag, Tooltip } from 'antd';
|
||||
import { OPERATORS } from 'constants/queryBuilder';
|
||||
import { LogsExplorerShortcuts } from 'constants/shortcuts/logsExplorerShortcuts';
|
||||
import { getDataTypes } from 'container/LogDetailedView/utils';
|
||||
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||
import {
|
||||
useAutoComplete,
|
||||
WhereClauseConfig,
|
||||
} from 'hooks/queryBuilder/useAutoComplete';
|
||||
import { useFetchKeysAndValues } from 'hooks/queryBuilder/useFetchKeysAndValues';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import type { BaseSelectRef } from 'rc-select';
|
||||
import {
|
||||
KeyboardEvent,
|
||||
ReactElement,
|
||||
@ -15,6 +19,8 @@ import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
@ -63,12 +69,18 @@ function QueryBuilderSearch({
|
||||
searchKey,
|
||||
} = useAutoComplete(query, whereClauseConfig);
|
||||
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const selectRef = useRef<BaseSelectRef>(null);
|
||||
const { sourceKeys, handleRemoveSourceKey } = useFetchKeysAndValues(
|
||||
searchValue,
|
||||
query,
|
||||
searchKey,
|
||||
);
|
||||
|
||||
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
||||
|
||||
const { handleRunQuery } = useQueryBuilder();
|
||||
|
||||
const onTagRender = ({
|
||||
value,
|
||||
closable,
|
||||
@ -119,6 +131,13 @@ function QueryBuilderSearch({
|
||||
const onInputKeyDownHandler = (event: KeyboardEvent<Element>): void => {
|
||||
if (isMulti || event.key === 'Backspace') handleKeyDown(event);
|
||||
if (isExistsNotExistsOperator(searchValue)) handleKeyDown(event);
|
||||
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleRunQuery();
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeselect = useCallback(
|
||||
@ -185,6 +204,18 @@ function QueryBuilderSearch({
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
}, [sourceKeys]);
|
||||
|
||||
useEffect(() => {
|
||||
registerShortcut(LogsExplorerShortcuts.FocusTheSearchBar, () => {
|
||||
// set timeout is needed here else the select treats the hotkey as input value
|
||||
setTimeout(() => {
|
||||
selectRef.current?.focus();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
return (): void =>
|
||||
deregisterShortcut(LogsExplorerShortcuts.FocusTheSearchBar);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@ -192,11 +223,14 @@ function QueryBuilderSearch({
|
||||
}}
|
||||
>
|
||||
<Select
|
||||
ref={selectRef}
|
||||
getPopupContainer={popupContainer}
|
||||
virtual
|
||||
showSearch
|
||||
tagRender={onTagRender}
|
||||
filterOption={false}
|
||||
open={isOpen}
|
||||
onDropdownVisibleChange={setIsOpen}
|
||||
autoClearSearchValue={false}
|
||||
mode="multiple"
|
||||
placeholder={placeholder}
|
||||
@ -213,6 +247,7 @@ function QueryBuilderSearch({
|
||||
onInputKeyDown={onInputKeyDownHandler}
|
||||
notFoundContent={isFetching ? <Spin size="small" /> : null}
|
||||
suffixIcon={suffixIcon}
|
||||
showAction={['focus']}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<Select.Option key={option.label} value={option.value}>
|
||||
|
@ -35,6 +35,7 @@ import defaultMenuItems, {
|
||||
helpSupportMenuItem,
|
||||
inviteMemberMenuItem,
|
||||
manageLicenseMenuItem,
|
||||
shortcutMenuItem,
|
||||
slackSupportMenuItem,
|
||||
trySignozCloudMenuItem,
|
||||
} from './menuItems';
|
||||
@ -147,15 +148,6 @@ function SideNav({
|
||||
|
||||
const { t } = useTranslation('');
|
||||
|
||||
useEffect(() => {
|
||||
registerShortcut(GlobalShortcuts.SidebarCollapse, onCollapse);
|
||||
|
||||
return (): void => {
|
||||
deregisterShortcut(GlobalShortcuts.SidebarCollapse);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const isLicenseActive =
|
||||
licenseData?.payload?.licenses?.find((e: License) => e.isCurrent)?.status ===
|
||||
LICENSE_PLAN_STATUS.VALID;
|
||||
@ -172,6 +164,10 @@ function SideNav({
|
||||
);
|
||||
};
|
||||
|
||||
const onClickShortcuts = (): void => {
|
||||
history.push(`/shortcuts`);
|
||||
};
|
||||
|
||||
const onClickGetStarted = (): void => {
|
||||
history.push(`/get-started`);
|
||||
};
|
||||
@ -259,6 +255,42 @@ function SideNav({
|
||||
? ROUTES.ORG_SETTINGS
|
||||
: ROUTES.SETTINGS;
|
||||
|
||||
useEffect(() => {
|
||||
registerShortcut(GlobalShortcuts.SidebarCollapse, onCollapse);
|
||||
|
||||
registerShortcut(GlobalShortcuts.NavigateToServices, () =>
|
||||
onClickHandler(ROUTES.APPLICATION),
|
||||
);
|
||||
registerShortcut(GlobalShortcuts.NavigateToTraces, () =>
|
||||
onClickHandler(ROUTES.TRACE),
|
||||
);
|
||||
|
||||
registerShortcut(GlobalShortcuts.NavigateToLogs, () =>
|
||||
onClickHandler(ROUTES.LOGS),
|
||||
);
|
||||
|
||||
registerShortcut(GlobalShortcuts.NavigateToDashboards, () =>
|
||||
onClickHandler(ROUTES.ALL_DASHBOARD),
|
||||
);
|
||||
|
||||
registerShortcut(GlobalShortcuts.NavigateToAlerts, () =>
|
||||
onClickHandler(ROUTES.LIST_ALL_ALERT),
|
||||
);
|
||||
registerShortcut(GlobalShortcuts.NavigateToExceptions, () =>
|
||||
onClickHandler(ROUTES.ALL_ERROR),
|
||||
);
|
||||
|
||||
return (): void => {
|
||||
deregisterShortcut(GlobalShortcuts.SidebarCollapse);
|
||||
deregisterShortcut(GlobalShortcuts.NavigateToServices);
|
||||
deregisterShortcut(GlobalShortcuts.NavigateToTraces);
|
||||
deregisterShortcut(GlobalShortcuts.NavigateToLogs);
|
||||
deregisterShortcut(GlobalShortcuts.NavigateToDashboards);
|
||||
deregisterShortcut(GlobalShortcuts.NavigateToAlerts);
|
||||
deregisterShortcut(GlobalShortcuts.NavigateToExceptions);
|
||||
};
|
||||
}, [deregisterShortcut, onClickHandler, onCollapse, registerShortcut]);
|
||||
|
||||
return (
|
||||
<div className={cx('sideNav', collapsed ? 'collapsed' : '')}>
|
||||
<div className="brand">
|
||||
@ -309,6 +341,14 @@ function SideNav({
|
||||
</div>
|
||||
|
||||
<div className="secondary-nav-items">
|
||||
<NavItem
|
||||
isCollapsed={collapsed}
|
||||
key="keyboardShortcuts"
|
||||
item={shortcutMenuItem}
|
||||
isActive={false}
|
||||
onClick={onClickShortcuts}
|
||||
/>
|
||||
|
||||
{licenseData && !isLicenseActive && (
|
||||
<NavItem
|
||||
isCollapsed={collapsed}
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
Cloudy,
|
||||
DraftingCompass,
|
||||
FileKey2,
|
||||
Layers2,
|
||||
LayoutGrid,
|
||||
MessageSquare,
|
||||
Receipt,
|
||||
@ -44,6 +45,12 @@ export const helpSupportMenuItem = {
|
||||
icon: <MessageSquare size={16} />,
|
||||
};
|
||||
|
||||
export const shortcutMenuItem = {
|
||||
key: ROUTES.SHORTCUTS,
|
||||
label: 'Keyboard Shortcuts',
|
||||
icon: <Layers2 size={16} />,
|
||||
};
|
||||
|
||||
export const slackSupportMenuItem = {
|
||||
key: SecondaryMenuItemKey.Slack,
|
||||
label: 'Slack Support',
|
||||
|
@ -133,6 +133,7 @@ export const routesToSkip = [
|
||||
ROUTES.LOGS_PIPELINES,
|
||||
ROUTES.TRACES_EXPLORER,
|
||||
ROUTES.TRACES_SAVE_VIEWS,
|
||||
ROUTES.SHORTCUTS,
|
||||
];
|
||||
|
||||
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];
|
||||
|
24
frontend/src/pages/Shortcuts/Shortcuts.styles.scss
Normal file
24
frontend/src/pages/Shortcuts/Shortcuts.styles.scss
Normal file
@ -0,0 +1,24 @@
|
||||
.keyboard-shortcuts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
gap: 50px;
|
||||
|
||||
.shortcut-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
|
||||
.shortcut-section-heading {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-weight: 600;
|
||||
font-size: 22px;
|
||||
line-height: 1.3636363636363635;
|
||||
}
|
||||
|
||||
.shortcut-section-table {
|
||||
width: 70%;
|
||||
}
|
||||
}
|
||||
}
|
35
frontend/src/pages/Shortcuts/Shortcuts.tsx
Normal file
35
frontend/src/pages/Shortcuts/Shortcuts.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import './Shortcuts.styles.scss';
|
||||
|
||||
import { Table, Typography } from 'antd';
|
||||
|
||||
import { ALL_SHORTCUTS, generateTableData, shortcutColumns } from './utils';
|
||||
|
||||
function Shortcuts(): JSX.Element {
|
||||
function getShortcutTable(shortcutSection: string): JSX.Element {
|
||||
const tableData = generateTableData(shortcutSection);
|
||||
|
||||
return (
|
||||
<section className="shortcut-section">
|
||||
<Typography.Text className="shortcut-section-heading">
|
||||
{shortcutSection}
|
||||
</Typography.Text>
|
||||
<Table
|
||||
columns={shortcutColumns}
|
||||
dataSource={tableData}
|
||||
pagination={false}
|
||||
className="shortcut-section-table"
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="keyboard-shortcuts">
|
||||
{Object.keys(ALL_SHORTCUTS).map((shortcutSection) =>
|
||||
getShortcutTable(shortcutSection),
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Shortcuts;
|
3
frontend/src/pages/Shortcuts/index.ts
Normal file
3
frontend/src/pages/Shortcuts/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import Shortcuts from './Shortcuts';
|
||||
|
||||
export default Shortcuts;
|
54
frontend/src/pages/Shortcuts/utils.ts
Normal file
54
frontend/src/pages/Shortcuts/utils.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { TableProps } from 'antd';
|
||||
import {
|
||||
GlobalShortcuts,
|
||||
GlobalShortcutsDescription,
|
||||
} from 'constants/shortcuts/globalShortcuts';
|
||||
import {
|
||||
LogsExplorerShortcuts,
|
||||
LogsExplorerShortcutsDescription,
|
||||
} from 'constants/shortcuts/logsExplorerShortcuts';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export const ALL_SHORTCUTS: Record<string, Record<string, string>> = {
|
||||
'Global Shortcuts': GlobalShortcuts,
|
||||
'Logs Explorer Shortcuts': LogsExplorerShortcuts,
|
||||
};
|
||||
|
||||
export const ALL_SHORTCUTS_DESCRIPTION: Record<
|
||||
string,
|
||||
Record<string, string>
|
||||
> = {
|
||||
'Global Shortcuts': GlobalShortcutsDescription,
|
||||
'Logs Explorer Shortcuts': LogsExplorerShortcutsDescription,
|
||||
};
|
||||
|
||||
export const shortcutColumns = [
|
||||
{
|
||||
title: 'Keyboard Shortcut',
|
||||
dataIndex: 'shortcutKey',
|
||||
key: 'shortcutKey',
|
||||
width: '30%',
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
dataIndex: 'shortcutDescription',
|
||||
key: 'shortcutDescription',
|
||||
},
|
||||
];
|
||||
|
||||
interface ShortcutRow {
|
||||
shortcutKey: string;
|
||||
shortcutDescription: string;
|
||||
}
|
||||
|
||||
export function generateTableData(
|
||||
shortcutSection: string,
|
||||
): TableProps<ShortcutRow>['dataSource'] {
|
||||
const shortcuts = ALL_SHORTCUTS[shortcutSection];
|
||||
const shortcutsDescription = ALL_SHORTCUTS_DESCRIPTION[shortcutSection];
|
||||
return Object.keys(shortcuts).map((shortcutName) => ({
|
||||
key: `${shortcuts[shortcutName]} ${shortcutName}`,
|
||||
shortcutKey: shortcuts[shortcutName],
|
||||
shortcutDescription: shortcutsDescription[shortcutName],
|
||||
}));
|
||||
}
|
@ -90,4 +90,5 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
|
||||
TRACES_SAVE_VIEWS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
LOGS_BASE: [],
|
||||
OLD_LOGS_EXPLORER: [],
|
||||
SHORTCUTS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user