mirror of
https://github.com/SigNoz/signoz.git
synced 2026-08-19 11:20:39 +01:00
Compare commits
3 Commits
fix/panel-
...
fix/log-v2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a3106cbc8 | ||
|
|
ae492b2bed | ||
|
|
a6908bad50 |
@@ -19,6 +19,7 @@ export type LogDetailProps = {
|
||||
onScrollToLog?: (logId: string) => void;
|
||||
handleOpenInExplorer?: MouseEventHandler;
|
||||
getContainer?: DrawerProps['getContainer'];
|
||||
onApplyLogFilter?: (expression: string) => void;
|
||||
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
|
||||
Partial<Pick<ActionItemProps, 'onClickActionItem'>> &
|
||||
Pick<DrawerProps, 'onClose'>;
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
Link,
|
||||
} from '@signozhq/icons';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { normalizeTimeToMs } from 'utils/timeUtils';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { MouseEvent, MouseEventHandler } from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
@@ -67,6 +68,11 @@ function LogDetailsHeader({
|
||||
},
|
||||
];
|
||||
|
||||
const rawTimestamp = log.date ?? log.timestamp;
|
||||
const displayTimestamp = Number.isNaN(Number(rawTimestamp))
|
||||
? rawTimestamp
|
||||
: normalizeTimeToMs(rawTimestamp);
|
||||
|
||||
return (
|
||||
<div className={styles.header} data-log-detail-ignore="true">
|
||||
<div className={styles.leftSection}>
|
||||
@@ -76,7 +82,7 @@ function LogDetailsHeader({
|
||||
data-testid="log-details-header-timestamp"
|
||||
>
|
||||
{formatTimezoneAdjustedTimestamp(
|
||||
log.date ?? log.timestamp,
|
||||
displayTimestamp,
|
||||
DATE_TIME_FORMATS.DASH_DATETIME,
|
||||
)}
|
||||
</Typography.Text>
|
||||
|
||||
@@ -75,6 +75,7 @@ function LogDetailInner({
|
||||
onScrollToLog,
|
||||
handleOpenInExplorer,
|
||||
getContainer,
|
||||
onApplyLogFilter,
|
||||
}: LogDetailInnerProps): JSX.Element {
|
||||
const initialContextQuery = useInitialQuery(log);
|
||||
const [contextQuery, setContextQuery] = useState<Query | undefined>(
|
||||
@@ -519,6 +520,7 @@ function LogDetailInner({
|
||||
selectedOptions={options}
|
||||
listViewPanelSelectedFields={listViewPanelSelectedFields}
|
||||
handleChangeSelectedView={handleChangeSelectedView}
|
||||
onApplyLogFilter={onApplyLogFilter}
|
||||
/>
|
||||
)}
|
||||
{!isLogDetailsV2 && selectedView === VIEW_TYPES.JSON && (
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
// v2 is rolled out only on the logs explorer route for now; every other surface
|
||||
// (dashboards, infra monitoring, etc.) keeps the v1 log details view.
|
||||
export function useIsLogDetailsV2(): boolean {
|
||||
const { pathname } = useLocation();
|
||||
return pathname === ROUTES.LOGS_EXPLORER;
|
||||
return (
|
||||
pathname === ROUTES.LOGS_EXPLORER ||
|
||||
pathname.startsWith(ROUTES.INFRASTRUCTURE_MONITORING_BASE) ||
|
||||
pathname.startsWith(`${ROUTES.ALL_DASHBOARD}/`)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -139,6 +139,11 @@ export default function AlertRules({
|
||||
encodeURIComponent(JSON.stringify(compositeQuery)),
|
||||
);
|
||||
|
||||
const panelType = record.condition.compositeQuery.panelType;
|
||||
if (panelType) {
|
||||
params.set(QueryParams.panelTypes, panelType);
|
||||
}
|
||||
|
||||
params.set(QueryParams.ruleId, record.id);
|
||||
|
||||
history.push(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
|
||||
|
||||
@@ -82,6 +82,7 @@ function EntityLogsContent({
|
||||
const { activeLog, selectedTab, handleSetActiveLog, handleCloseLogDetail } =
|
||||
useLogDetailHandlers();
|
||||
|
||||
// TODO: Move away from using onAddToQuery after old drawer cleanup
|
||||
const onAddToQuery = useCallback(
|
||||
(fieldKey: string, fieldValue: string, operator: string): void => {
|
||||
handleCloseLogDetail();
|
||||
@@ -104,6 +105,21 @@ function EntityLogsContent({
|
||||
[userExpression, querySearchOnRun, handleCloseLogDetail, category],
|
||||
);
|
||||
|
||||
const onApplyLogFilter = useCallback(
|
||||
(expression: string): void => {
|
||||
handleCloseLogDetail();
|
||||
|
||||
const newUser = userExpression.trim()
|
||||
? `${userExpression} AND ${expression}`
|
||||
: expression;
|
||||
|
||||
querySearchOnRun(newUser);
|
||||
|
||||
logInfraDrawerFilterCustomizedEvent(category, 'logs', newUser, 'logs');
|
||||
},
|
||||
[userExpression, querySearchOnRun, handleCloseLogDetail, category],
|
||||
);
|
||||
|
||||
const {
|
||||
logs,
|
||||
loadMoreLogs,
|
||||
@@ -328,6 +344,7 @@ function EntityLogsContent({
|
||||
selectedTab={selectedTab}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onClickActionItem={onAddToQuery}
|
||||
onApplyLogFilter={onApplyLogFilter}
|
||||
onScrollToLog={handleScrollToLog}
|
||||
handleOpenInExplorer={(e) => handleOpenInExplorer(e, activeLog)}
|
||||
getContainer={(): HTMLElement =>
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('ListAlertRules — row click navigation', () => {
|
||||
const [url] = safeNavigateMock.mock.calls[0];
|
||||
expect(url).toContain('/alerts/overview?');
|
||||
expect(url).toContain('ruleId=rule-1');
|
||||
expect(url).not.toContain('panelTypes');
|
||||
expect(url).toContain('panelTypes=graph');
|
||||
expect(url).toContain('compositeQuery=');
|
||||
});
|
||||
|
||||
|
||||
@@ -36,6 +36,11 @@ export function useAlertRulesHandlers(
|
||||
encodeURIComponent(JSON.stringify(compositeQuery)),
|
||||
);
|
||||
|
||||
const panelType = rule.condition.compositeQuery.panelType;
|
||||
if (panelType) {
|
||||
params.set(QueryParams.panelTypes, panelType);
|
||||
}
|
||||
|
||||
params.set(QueryParams.ruleId, rule.id);
|
||||
|
||||
return `${ROUTES.ALERT_OVERVIEW}?${params.toString()}`;
|
||||
|
||||
@@ -41,6 +41,7 @@ interface OverviewProps {
|
||||
selectedOptions: OptionsQuery;
|
||||
listViewPanelSelectedFields?: IField[] | null;
|
||||
handleChangeSelectedView?: ChangeViewFunctionType;
|
||||
onApplyLogFilter?: (expression: string) => void;
|
||||
}
|
||||
|
||||
type Props = OverviewProps &
|
||||
@@ -55,6 +56,7 @@ function Overview({
|
||||
selectedOptions,
|
||||
listViewPanelSelectedFields,
|
||||
handleChangeSelectedView,
|
||||
onApplyLogFilter,
|
||||
}: Props): JSX.Element {
|
||||
const [isWrapWord, setIsWrapWord] = useState<boolean>(true);
|
||||
const [isSearchVisible, setIsSearchVisible] = useState<boolean>(true);
|
||||
@@ -67,6 +69,7 @@ function Overview({
|
||||
const { actions, visibleActions } = useLogAttributeActions({
|
||||
handleChangeSelectedView,
|
||||
isListViewPanel,
|
||||
onApplyLogFilter,
|
||||
});
|
||||
|
||||
const isLogDetailsV2 = useIsLogDetailsV2();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { CircleMinus, CirclePlus, Layers, RefreshCw } from '@signozhq/icons';
|
||||
import { convertFiltersToExpression } from 'components/QueryBuilderV2/utils';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
VisibleActionsConfig,
|
||||
} from 'periscope/components/PrettyView/PrettyView';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { LogDetailsAction } from '../constants';
|
||||
import {
|
||||
@@ -27,6 +29,7 @@ import {
|
||||
interface UseLogAttributeActionsParams {
|
||||
handleChangeSelectedView?: ChangeViewFunctionType;
|
||||
isListViewPanel?: boolean;
|
||||
onApplyLogFilter?: (expression: string) => void;
|
||||
}
|
||||
|
||||
interface UseLogAttributeActionsResult {
|
||||
@@ -50,6 +53,7 @@ const ALL_LEAF_ACTIONS = [
|
||||
export function useLogAttributeActions({
|
||||
handleChangeSelectedView,
|
||||
isListViewPanel = false,
|
||||
onApplyLogFilter,
|
||||
}: UseLogAttributeActionsParams): UseLogAttributeActionsResult {
|
||||
const { pathname } = useLocation();
|
||||
const { stagedQuery, updateQueriesData } = useQueryBuilder();
|
||||
@@ -65,9 +69,6 @@ export function useLogAttributeActions({
|
||||
|
||||
const filterFor = useCallback(
|
||||
(context: FieldContext, isFilterIn: boolean): void => {
|
||||
if (!stagedQuery) {
|
||||
return;
|
||||
}
|
||||
const target = buildLogFilterTarget(
|
||||
context.fieldKeyPath,
|
||||
context.fieldValue,
|
||||
@@ -77,6 +78,29 @@ export function useLogAttributeActions({
|
||||
? target.filterInOperator
|
||||
: target.filterOutOperator;
|
||||
|
||||
// Non-explorer surfaces (infra monitoring, etc.) apply a ready v5
|
||||
// expression fragment to their own query.
|
||||
if (onApplyLogFilter) {
|
||||
const base = {
|
||||
filters: { items: [], op: 'AND' },
|
||||
} as unknown as IBuilderQuery;
|
||||
const nextFilters = getFilterQueryData(
|
||||
base,
|
||||
target,
|
||||
context.fieldValue,
|
||||
operator,
|
||||
).filters ?? { items: [], op: 'AND' };
|
||||
const { expression } = convertFiltersToExpression(nextFilters);
|
||||
if (expression) {
|
||||
onApplyLogFilter(expression);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!stagedQuery) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedQuery = updateQueriesData(
|
||||
stagedQuery,
|
||||
'queryData',
|
||||
@@ -99,6 +123,7 @@ export function useLogAttributeActions({
|
||||
updateQueriesData,
|
||||
viewName,
|
||||
handleChangeSelectedView,
|
||||
onApplyLogFilter,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -179,20 +204,25 @@ export function useLogAttributeActions({
|
||||
buildLogFilterTarget(fieldKeyPath, undefined, isBodyJsonQueryEnabled)
|
||||
.isRestricted;
|
||||
|
||||
// The using surface must provide an apply path.
|
||||
const canApplyFilter = !!handleChangeSelectedView || !!onApplyLogFilter;
|
||||
|
||||
return [
|
||||
{
|
||||
key: LogDetailsAction.FILTER_IN,
|
||||
label: 'Filter for value',
|
||||
icon: <CirclePlus size={12} />,
|
||||
onClick: (context): void => filterFor(context, true),
|
||||
shouldHide: (_key, fieldKeyPath): boolean => isRestricted(fieldKeyPath),
|
||||
shouldHide: (_key, fieldKeyPath): boolean =>
|
||||
!canApplyFilter || isRestricted(fieldKeyPath),
|
||||
},
|
||||
{
|
||||
key: LogDetailsAction.FILTER_OUT,
|
||||
label: 'Filter out value',
|
||||
icon: <CircleMinus size={12} />,
|
||||
onClick: (context): void => filterFor(context, false),
|
||||
shouldHide: (_key, fieldKeyPath): boolean => isRestricted(fieldKeyPath),
|
||||
shouldHide: (_key, fieldKeyPath): boolean =>
|
||||
!canApplyFilter || isRestricted(fieldKeyPath),
|
||||
},
|
||||
{
|
||||
key: LogDetailsAction.GROUP_BY,
|
||||
@@ -200,8 +230,10 @@ export function useLogAttributeActions({
|
||||
icon: <Layers size={12} />,
|
||||
onClick: groupBy,
|
||||
shouldHide: (_key, fieldKeyPath): boolean =>
|
||||
!handleChangeSelectedView ||
|
||||
!buildLogFilterTarget(fieldKeyPath, undefined, isBodyJsonQueryEnabled)
|
||||
.groupBySupported || isOldExplorerOrLive,
|
||||
.groupBySupported ||
|
||||
isOldExplorerOrLive,
|
||||
},
|
||||
{
|
||||
key: LogDetailsAction.REPLACE_FILTER,
|
||||
@@ -209,7 +241,9 @@ export function useLogAttributeActions({
|
||||
icon: <RefreshCw size={12} />,
|
||||
onClick: replaceFilter,
|
||||
shouldHide: (_key, fieldKeyPath): boolean =>
|
||||
isRestricted(fieldKeyPath) || isOldExplorerOrLive,
|
||||
!handleChangeSelectedView ||
|
||||
isRestricted(fieldKeyPath) ||
|
||||
isOldExplorerOrLive,
|
||||
},
|
||||
];
|
||||
}, [
|
||||
@@ -218,6 +252,8 @@ export function useLogAttributeActions({
|
||||
replaceFilter,
|
||||
isBodyJsonQueryEnabled,
|
||||
isOldExplorerOrLive,
|
||||
handleChangeSelectedView,
|
||||
onApplyLogFilter,
|
||||
]);
|
||||
|
||||
const visibleActions = useMemo<VisibleActionsConfig>(
|
||||
|
||||
@@ -95,6 +95,7 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
|
||||
QueryParams.compositeQuery,
|
||||
encodeURIComponent(JSON.stringify(updatedQuery)),
|
||||
);
|
||||
params.set(QueryParams.panelTypes, widget.panelTypes);
|
||||
params.set(QueryParams.version, ENTITY_VERSION_V5);
|
||||
params.set(QueryParams.source, YAxisSource.DASHBOARDS);
|
||||
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import { Router } from 'react-router-dom';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { createMemoryHistory } from 'history';
|
||||
|
||||
import { useGetPanelTypesQueryParam } from './useGetPanelTypesQueryParam';
|
||||
|
||||
const renderWithSearch = (
|
||||
search: string,
|
||||
defaultPanelType?: PANEL_TYPES,
|
||||
): PANEL_TYPES | null => {
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: [`/logs/logs-explorer${search}`],
|
||||
});
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useGetPanelTypesQueryParam(defaultPanelType),
|
||||
{
|
||||
wrapper: ({ children }) => <Router history={history}>{children}</Router>,
|
||||
},
|
||||
);
|
||||
|
||||
return result.current;
|
||||
};
|
||||
|
||||
describe('useGetPanelTypesQueryParam', () => {
|
||||
it('reads a JSON encoded panel type, as written by the explorers', () => {
|
||||
expect(renderWithSearch('?panelTypes=%22table%22', PANEL_TYPES.LIST)).toBe(
|
||||
PANEL_TYPES.TABLE,
|
||||
);
|
||||
});
|
||||
|
||||
it('reads a plain string panel type, as written by the alerts flow', () => {
|
||||
expect(renderWithSearch('?panelTypes=graph', PANEL_TYPES.LIST)).toBe(
|
||||
PANEL_TYPES.TIME_SERIES,
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to the default for an unparseable panel type', () => {
|
||||
expect(renderWithSearch('?panelTypes=%7Bfoo', PANEL_TYPES.LIST)).toBe(
|
||||
PANEL_TYPES.LIST,
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to the default for a value that is not a panel type', () => {
|
||||
expect(renderWithSearch('?panelTypes=%22nope%22', PANEL_TYPES.LIST)).toBe(
|
||||
PANEL_TYPES.LIST,
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to the default when the param is absent', () => {
|
||||
expect(renderWithSearch('', PANEL_TYPES.LIST)).toBe(PANEL_TYPES.LIST);
|
||||
});
|
||||
});
|
||||
@@ -3,24 +3,6 @@ import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
|
||||
const PANEL_TYPE_VALUES = new Set<string>(Object.values(PANEL_TYPES));
|
||||
|
||||
// The param is JSON encoded by the explorers and written as a plain string by the
|
||||
// alerts flow, so accept both and treat anything unrecognised as absent.
|
||||
const parsePanelType = (value: string): PANEL_TYPES | null => {
|
||||
let parsed: unknown = value;
|
||||
|
||||
try {
|
||||
parsed = JSON.parse(value);
|
||||
} catch {
|
||||
parsed = value;
|
||||
}
|
||||
|
||||
return typeof parsed === 'string' && PANEL_TYPE_VALUES.has(parsed)
|
||||
? (parsed as PANEL_TYPES)
|
||||
: null;
|
||||
};
|
||||
|
||||
export const useGetPanelTypesQueryParam = <T extends PANEL_TYPES | undefined>(
|
||||
defaultPanelType?: T,
|
||||
): T extends undefined ? PANEL_TYPES | null : PANEL_TYPES => {
|
||||
@@ -29,10 +11,6 @@ export const useGetPanelTypesQueryParam = <T extends PANEL_TYPES | undefined>(
|
||||
return useMemo(() => {
|
||||
const panelTypeQuery = urlQuery.get(QueryParams.panelTypes);
|
||||
|
||||
return (
|
||||
(panelTypeQuery ? parsePanelType(panelTypeQuery) : null) ?? defaultPanelType
|
||||
);
|
||||
}, [urlQuery, defaultPanelType]) as T extends undefined
|
||||
? PANEL_TYPES | null
|
||||
: PANEL_TYPES;
|
||||
return panelTypeQuery ? JSON.parse(panelTypeQuery) : defaultPanelType;
|
||||
}, [urlQuery, defaultPanelType]);
|
||||
};
|
||||
|
||||
@@ -168,6 +168,7 @@ describe('useCreateAlertFromPanel', () => {
|
||||
// The resolved query is seeded with the panel-derived alert prefill.
|
||||
expect(mockBuildAlertUrl).toHaveBeenCalledWith(
|
||||
{ resolved: 'query' },
|
||||
PANEL_TYPES.TIME_SERIES,
|
||||
undefined,
|
||||
mockPrefill,
|
||||
);
|
||||
|
||||
@@ -79,6 +79,7 @@ export function useCreateAlertFromPanel(): (
|
||||
const unit = readPanelUnit(panel.spec.plugin);
|
||||
const url = buildAlertUrl(
|
||||
query,
|
||||
panelType,
|
||||
unit,
|
||||
deriveAlertPrefill(panel, query, unit),
|
||||
);
|
||||
|
||||
@@ -65,19 +65,14 @@ describe('buildCreateAlertUrl', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('tags the URL with the v5 version and the dashboards source', () => {
|
||||
it('tags the URL with panel type, v5 version, and the dashboards source', () => {
|
||||
const params = parse(buildCreateAlertUrl(makePanel()));
|
||||
|
||||
expect(params.get(QueryParams.panelTypes)).toBe(PANEL_TYPES.TIME_SERIES);
|
||||
expect(params.get(QueryParams.version)).toBe(ENTITY_VERSION_V5);
|
||||
expect(params.get(QueryParams.source)).toBe('dashboards');
|
||||
});
|
||||
|
||||
it('does not tag the URL with a panel type, which the alert page ignores', () => {
|
||||
const params = parse(buildCreateAlertUrl(makePanel()));
|
||||
|
||||
expect(params.get(QueryParams.panelTypes)).toBeNull();
|
||||
});
|
||||
|
||||
it('encodes the translated query as the compositeQuery param', () => {
|
||||
const params = parse(buildCreateAlertUrl(makePanel()));
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
import { YAxisSource } from 'components/YAxisUnitSelector/types';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { PANEL_KIND_TO_PANEL_TYPE } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/panelKind';
|
||||
import { fromPerses } from 'pages/DashboardPageV2/DashboardContainer/queryV5/persesQueryAdapters';
|
||||
@@ -33,6 +34,7 @@ export function readPanelUnit(
|
||||
*/
|
||||
export function buildAlertUrl(
|
||||
query: Query,
|
||||
panelType: PANEL_TYPES,
|
||||
unit?: string,
|
||||
prefill?: PanelAlertPrefill,
|
||||
): string {
|
||||
@@ -46,6 +48,7 @@ export function buildAlertUrl(
|
||||
QueryParams.compositeQuery,
|
||||
encodeURIComponent(JSON.stringify(query)),
|
||||
);
|
||||
params.set(QueryParams.panelTypes, panelType);
|
||||
params.set(QueryParams.version, ENTITY_VERSION_V5);
|
||||
params.set(QueryParams.source, YAxisSource.DASHBOARDS);
|
||||
|
||||
@@ -73,5 +76,10 @@ export function buildCreateAlertUrl(panel: DashboardtypesPanelDTO): string {
|
||||
const panelType = PANEL_KIND_TO_PANEL_TYPE[panel.spec.plugin.kind];
|
||||
const query = fromPerses(panel.spec.queries, panelType);
|
||||
const unit = readPanelUnit(panel.spec.plugin);
|
||||
return buildAlertUrl(query, unit, deriveAlertPrefill(panel, query, unit));
|
||||
return buildAlertUrl(
|
||||
query,
|
||||
panelType,
|
||||
unit,
|
||||
deriveAlertPrefill(panel, query, unit),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user