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:
Vikrant Gupta 2024-02-12 19:53:35 +05:30 committed by GitHub
parent 0e331dd177
commit 3a20862d0c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 262 additions and 10 deletions

View File

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

View File

@ -182,3 +182,7 @@ export const WorkspaceBlocked = Loadable(
() =>
import(/* webpackChunkName: "WorkspaceLocked" */ 'pages/WorkspaceLocked'),
);
export const ShortcutsPage = Loadable(
() => import(/* webpackChunkName: "ShortcutsPage" */ 'pages/Shortcuts'),
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

@ -0,0 +1,3 @@
import Shortcuts from './Shortcuts';
export default Shortcuts;

View 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],
}));
}

View File

@ -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'],
};