mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-11 03:54:28 +00:00
Compare commits
28 Commits
update-co
...
feat/cmd-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
862a5cf6bd | ||
|
|
af87c2e80b | ||
|
|
e1ac992e5a | ||
|
|
15161c09e8 | ||
|
|
ee5fbe41eb | ||
|
|
f2f3a7b24a | ||
|
|
dd0738ac70 | ||
|
|
1c4dfc931f | ||
|
|
605d6ba17d | ||
|
|
11c8ed305e | ||
|
|
bb0801a83f | ||
|
|
1c2fc57da1 | ||
|
|
ed8fbe2e4a | ||
|
|
260f5c39f5 | ||
|
|
1f137c5a9c | ||
|
|
17efa7672b | ||
|
|
80433b663d | ||
|
|
9bf68b276f | ||
|
|
874d67f43a | ||
|
|
2259e8c299 | ||
|
|
c3aa0e5b3b | ||
|
|
1f21efd0cf | ||
|
|
f648e3c684 | ||
|
|
db0c55cc53 | ||
|
|
9eb2a984f7 | ||
|
|
dd81ba1711 | ||
|
|
eed73013a4 | ||
|
|
c6fc2be670 |
@@ -12,6 +12,7 @@ linters:
|
||||
- misspell
|
||||
- nilnil
|
||||
- sloglint
|
||||
- wastedassign
|
||||
- unparam
|
||||
- unused
|
||||
settings:
|
||||
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"eslint.workingDirectories": ["./frontend"],
|
||||
"eslint.workingDirectories": [
|
||||
"./frontend"
|
||||
],
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.codeActionsOnSave": {
|
||||
|
||||
@@ -291,3 +291,12 @@ flagger:
|
||||
float:
|
||||
integer:
|
||||
object:
|
||||
|
||||
##################### User #####################
|
||||
user:
|
||||
password:
|
||||
reset:
|
||||
# Whether to allow users to reset their password themselves.
|
||||
allow_self: true
|
||||
# The duration within which a user can reset their password.
|
||||
max_token_lifetime: 6h
|
||||
|
||||
@@ -1985,6 +1985,35 @@ paths:
|
||||
summary: Update user preference
|
||||
tags:
|
||||
- preferences
|
||||
/api/v2/factor_password/forgot:
|
||||
post:
|
||||
deprecated: false
|
||||
description: This endpoint initiates the forgot password flow by sending a reset
|
||||
password email
|
||||
operationId: ForgotPassword
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TypesPostableForgotPassword'
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
summary: Forgot password
|
||||
tags:
|
||||
- users
|
||||
/api/v2/features:
|
||||
get:
|
||||
deprecated: false
|
||||
@@ -3979,6 +4008,15 @@ components:
|
||||
token:
|
||||
type: string
|
||||
type: object
|
||||
TypesPostableForgotPassword:
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
frontendBaseURL:
|
||||
type: string
|
||||
orgId:
|
||||
type: string
|
||||
type: object
|
||||
TypesPostableInvite:
|
||||
properties:
|
||||
email:
|
||||
@@ -3999,6 +4037,9 @@ components:
|
||||
type: object
|
||||
TypesResetPasswordToken:
|
||||
properties:
|
||||
expiresAt:
|
||||
format: date-time
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
passwordId:
|
||||
|
||||
@@ -38,7 +38,7 @@ module.exports = {
|
||||
'import', // Import/export linting
|
||||
'sonarjs', // Code quality/complexity
|
||||
// TODO: Uncomment after running: yarn add -D eslint-plugin-spellcheck
|
||||
// 'spellcheck',
|
||||
// 'spellcheck', // Correct spellings
|
||||
],
|
||||
settings: {
|
||||
react: {
|
||||
@@ -60,12 +60,18 @@ module.exports = {
|
||||
'no-debugger': 'error', // Disallows debugger statements in production code
|
||||
curly: 'error', // Requires curly braces for all control statements
|
||||
eqeqeq: ['error', 'always', { null: 'ignore' }], // Enforces === and !== (allows == null for null/undefined check)
|
||||
// TODO: Enable after fixing ~15 console.log statements
|
||||
// 'no-console': ['error', { allow: ['warn', 'error'] }], // Warns on console.log, allows console.warn/error
|
||||
'no-console': ['error', { allow: ['warn', 'error'] }], // Warns on console.log, allows console.warn/error
|
||||
|
||||
// TypeScript rules
|
||||
'@typescript-eslint/explicit-function-return-type': 'error', // Requires explicit return types on functions
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
// Disallows unused variables/args
|
||||
'error',
|
||||
{
|
||||
argsIgnorePattern: '^_', // Allows unused args prefixed with _ (e.g., _unusedParam)
|
||||
varsIgnorePattern: '^_', // Allows unused vars prefixed with _ (e.g., _unusedVar)
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-explicit-any': 'warn', // Warns when using 'any' type (consider upgrading to error)
|
||||
// TODO: Change to 'error' after fixing ~80 empty function placeholders in providers/contexts
|
||||
'@typescript-eslint/no-empty-function': 'off', // Disallows empty function bodies
|
||||
|
||||
@@ -41,7 +41,7 @@ export const getConsumerLagDetails = async (
|
||||
> => {
|
||||
const { detailType, ...restProps } = props;
|
||||
const response = await axios.post(
|
||||
`/messaging-queues/kafka/consumer-lag/${props.detailType}`,
|
||||
`/messaging-queues/kafka/consumer-lag/${detailType}`,
|
||||
{
|
||||
...restProps,
|
||||
},
|
||||
|
||||
@@ -43,16 +43,17 @@ export const omitIdFromQuery = (query: Query | null): any => ({
|
||||
builder: {
|
||||
...query?.builder,
|
||||
queryData: query?.builder.queryData.map((queryData) => {
|
||||
const { id, ...rest } = queryData.aggregateAttribute || {};
|
||||
const { id: _aggregateAttributeId, ...rest } =
|
||||
queryData.aggregateAttribute || {};
|
||||
const newAggregateAttribute = rest;
|
||||
const newGroupByAttributes = queryData.groupBy.map((groupByAttribute) => {
|
||||
const { id, ...rest } = groupByAttribute;
|
||||
const { id: _groupByAttributeId, ...rest } = groupByAttribute;
|
||||
return rest;
|
||||
});
|
||||
const newItems = queryData.filters?.items?.map((item) => {
|
||||
const { id, ...newItem } = item;
|
||||
const { id: _itemId, ...newItem } = item;
|
||||
if (item.key) {
|
||||
const { id, ...rest } = item.key;
|
||||
const { id: _keyId, ...rest } = item.key;
|
||||
return {
|
||||
...newItem,
|
||||
key: rest,
|
||||
|
||||
@@ -47,6 +47,7 @@ import { AppState } from 'store/reducers';
|
||||
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
|
||||
|
||||
import { RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants';
|
||||
import { LogDetailInnerProps, LogDetailProps } from './LogDetail.interfaces';
|
||||
@@ -135,7 +136,7 @@ function LogDetailInner({
|
||||
};
|
||||
|
||||
// Go to logs explorer page with the log data
|
||||
const handleOpenInExplorer = (): void => {
|
||||
const handleOpenInExplorer = (event: React.MouseEvent): void => {
|
||||
const queryParams = {
|
||||
[QueryParams.activeLogId]: `"${log?.id}"`,
|
||||
[QueryParams.startTime]: minTime?.toString() || '',
|
||||
@@ -148,7 +149,16 @@ function LogDetailInner({
|
||||
),
|
||||
),
|
||||
};
|
||||
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`);
|
||||
|
||||
if (isCtrlOrMMetaKey(event)) {
|
||||
window.open(
|
||||
`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`,
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
);
|
||||
} else {
|
||||
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleQueryExpressionChange = useCallback(
|
||||
|
||||
@@ -45,7 +45,6 @@ function Pre({
|
||||
}
|
||||
|
||||
function Code({
|
||||
node,
|
||||
inline,
|
||||
className = 'blog-code',
|
||||
children,
|
||||
|
||||
@@ -28,6 +28,7 @@ import React, {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { CustomMultiSelectProps, CustomTagProps, OptionData } from './types';
|
||||
@@ -922,7 +923,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
const lastVisibleChipIndex = getLastVisibleChipIndex();
|
||||
|
||||
// Handle special keyboard combinations
|
||||
const isCtrlOrCmd = e.ctrlKey || e.metaKey;
|
||||
const isCtrlOrCmd = isCtrlOrMMetaKey(e);
|
||||
|
||||
// Handle Ctrl+A (select all)
|
||||
if (isCtrlOrCmd && e.key === 'a') {
|
||||
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
QUERY_BUILDER_OPERATORS_BY_KEY_TYPE,
|
||||
queryOperatorSuggestions,
|
||||
} from 'constants/antlrQueryConstants';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useDebounce from 'hooks/useDebounce';
|
||||
import { debounce, isNull } from 'lodash-es';
|
||||
@@ -208,8 +207,6 @@ function QuerySearch({
|
||||
const lastValueRef = useRef<string>('');
|
||||
const isMountedRef = useRef<boolean>(true);
|
||||
|
||||
const { handleRunQuery } = useQueryBuilder();
|
||||
|
||||
const { selectedDashboard } = useDashboard();
|
||||
|
||||
const dynamicVariables = useMemo(
|
||||
|
||||
@@ -87,7 +87,7 @@ function TraceOperatorEditor({
|
||||
// Track if the query was changed externally (from props) vs internally (user input)
|
||||
const [isExternalQueryChange, setIsExternalQueryChange] = useState(false);
|
||||
const [lastExternalValue, setLastExternalValue] = useState<string>('');
|
||||
const { currentQuery, handleRunQuery } = useQueryBuilder();
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const queryOptions = useMemo(
|
||||
() =>
|
||||
|
||||
@@ -5,7 +5,6 @@ import { EditorView } from '@uiw/react-codemirror';
|
||||
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
|
||||
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import * as UseQBModule from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { fireEvent, render, userEvent, waitFor } from 'tests/test-utils';
|
||||
import type { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/types';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
@@ -121,13 +120,8 @@ jest.mock('api/querySuggestions/getValueSuggestion', () => ({
|
||||
// Note: We're NOT mocking CodeMirror here - using the real component
|
||||
// This provides integration testing with the actual CodeMirror editor
|
||||
|
||||
const handleRunQueryMock = ((UseQBModule as unknown) as {
|
||||
handleRunQuery: jest.MockedFunction<() => void>;
|
||||
}).handleRunQuery;
|
||||
|
||||
const SAMPLE_KEY_TYPING = 'http.';
|
||||
const SAMPLE_VALUE_TYPING_INCOMPLETE = "service.name = '";
|
||||
const SAMPLE_VALUE_TYPING_COMPLETE = "service.name = 'frontend'";
|
||||
const SAMPLE_STATUS_QUERY = "http.status_code = '200'";
|
||||
|
||||
describe('QuerySearch (Integration with Real CodeMirror)', () => {
|
||||
|
||||
@@ -796,12 +796,12 @@ export const adjustQueryForV5 = (currentQuery: Query): Query => {
|
||||
});
|
||||
|
||||
const {
|
||||
aggregateAttribute,
|
||||
aggregateOperator,
|
||||
timeAggregation,
|
||||
spaceAggregation,
|
||||
reduceTo,
|
||||
filters,
|
||||
aggregateAttribute: _aggregateAttribute,
|
||||
aggregateOperator: _aggregateOperator,
|
||||
timeAggregation: _timeAggregation,
|
||||
spaceAggregation: _spaceAggregation,
|
||||
reduceTo: _reduceTo,
|
||||
filters: _filters,
|
||||
...retainedQuery
|
||||
} = query;
|
||||
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
import './Slider.styles.scss';
|
||||
|
||||
import { IQuickFiltersConfig } from 'components/QuickFilters/types';
|
||||
|
||||
interface ISliderProps {
|
||||
filter: IQuickFiltersConfig;
|
||||
}
|
||||
|
||||
// not needed for now build when required
|
||||
export default function Slider(props: ISliderProps): JSX.Element {
|
||||
const { filter } = props;
|
||||
console.log(filter);
|
||||
export default function Slider(): JSX.Element {
|
||||
return <div>Slider</div>;
|
||||
}
|
||||
|
||||
@@ -303,15 +303,9 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
/>
|
||||
);
|
||||
case FiltersType.DURATION:
|
||||
return (
|
||||
<Duration
|
||||
filter={filter}
|
||||
onFilterChange={onFilterChange}
|
||||
source={source}
|
||||
/>
|
||||
);
|
||||
return <Duration filter={filter} onFilterChange={onFilterChange} />;
|
||||
case FiltersType.SLIDER:
|
||||
return <Slider filter={filter} />;
|
||||
return <Slider />;
|
||||
// eslint-disable-next-line sonarjs/no-duplicated-branches
|
||||
default:
|
||||
return (
|
||||
|
||||
@@ -5,12 +5,12 @@ import { ResizeTable } from 'components/ResizeTable';
|
||||
import ROUTES from 'constants/routes';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import { Channels } from 'types/api/channels/getAll';
|
||||
import { genericNavigate } from 'utils/genericNavigate';
|
||||
|
||||
import Delete from './Delete';
|
||||
|
||||
@@ -20,13 +20,15 @@ function AlertChannels({ allChannels }: AlertChannelsProps): JSX.Element {
|
||||
const { user } = useAppContext();
|
||||
const [action] = useComponentPermission(['new_alert_action'], user.role);
|
||||
|
||||
const onClickEditHandler = useCallback((id: string) => {
|
||||
history.push(
|
||||
generatePath(ROUTES.CHANNELS_EDIT, {
|
||||
channelId: id,
|
||||
}),
|
||||
);
|
||||
}, []);
|
||||
const onClickEditHandler = useCallback(
|
||||
(id: string, event: React.MouseEvent): void => {
|
||||
genericNavigate(
|
||||
generatePath(ROUTES.CHANNELS_EDIT, { channelId: id }),
|
||||
event,
|
||||
);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const columns: ColumnsType<Channels> = [
|
||||
{
|
||||
@@ -52,7 +54,10 @@ function AlertChannels({ allChannels }: AlertChannelsProps): JSX.Element {
|
||||
width: 80,
|
||||
render: (id: string): JSX.Element => (
|
||||
<>
|
||||
<Button onClick={(): void => onClickEditHandler(id)} type="link">
|
||||
<Button
|
||||
onClick={(event: React.MouseEvent): void => onClickEditHandler(id, event)}
|
||||
type="link"
|
||||
>
|
||||
{t('column_channel_edit')}
|
||||
</Button>
|
||||
<Delete id={id} notifications={notifications} />
|
||||
|
||||
@@ -8,7 +8,6 @@ import Spinner from 'components/Spinner';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import ROUTES from 'constants/routes';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import history from 'lib/history';
|
||||
import { isUndefined } from 'lodash-es';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
@@ -17,6 +16,7 @@ import { useQuery } from 'react-query';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
import { Channels } from 'types/api/channels/getAll';
|
||||
import APIError from 'types/api/error';
|
||||
import { genericNavigate } from 'utils/genericNavigate';
|
||||
|
||||
import AlertChannelsComponent from './AlertChannels';
|
||||
import { Button, ButtonContainer, RightActionContainer } from './styles';
|
||||
@@ -30,8 +30,8 @@ function AlertChannels(): JSX.Element {
|
||||
['add_new_channel'],
|
||||
user.role,
|
||||
);
|
||||
const onToggleHandler = useCallback(() => {
|
||||
history.push(ROUTES.CHANNELS_NEW);
|
||||
const onToggleHandler = useCallback((event: React.MouseEvent) => {
|
||||
genericNavigate(ROUTES.CHANNELS_NEW, event);
|
||||
}, []);
|
||||
|
||||
const { isLoading, data, error } = useQuery<
|
||||
@@ -78,7 +78,7 @@ function AlertChannels(): JSX.Element {
|
||||
}
|
||||
>
|
||||
<Button
|
||||
onClick={onToggleHandler}
|
||||
onClick={(event: React.MouseEvent): void => onToggleHandler(event)}
|
||||
icon={<PlusOutlined />}
|
||||
disabled={!addNewChannelPermission}
|
||||
>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { defaultPostableAlertRuleV2 } from 'container/CreateAlertV2/constants';
|
||||
import { getCreateAlertLocalStateFromAlertDef } from 'container/CreateAlertV2/utils';
|
||||
|
||||
@@ -73,7 +73,7 @@ export function sanitizeDashboardData(
|
||||
|
||||
const updatedVariables = Object.entries(selectedData.variables).reduce(
|
||||
(acc, [key, value]) => {
|
||||
const { selectedValue, ...rest } = value;
|
||||
const { selectedValue: _selectedValue, ...rest } = value;
|
||||
acc[key] = rest;
|
||||
return acc;
|
||||
},
|
||||
|
||||
@@ -18,6 +18,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { PayloadProps as GetByErrorTypeAndServicePayload } from 'types/api/errors/getByErrorTypeAndService';
|
||||
import { genericNavigate } from 'utils/genericNavigate';
|
||||
|
||||
import { keyToExclude } from './config';
|
||||
import { DashedContainer, EditorContainer, EventContainer } from './styles';
|
||||
@@ -111,14 +112,18 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
||||
value: errorDetail[key as keyof GetByErrorTypeAndServicePayload],
|
||||
}));
|
||||
|
||||
const onClickTraceHandler = (): void => {
|
||||
const onClickTraceHandler = (event: React.MouseEvent): void => {
|
||||
logEvent('Exception: Navigate to trace detail page', {
|
||||
groupId: errorDetail?.groupID,
|
||||
spanId: errorDetail.spanID,
|
||||
traceId: errorDetail.traceID,
|
||||
exceptionId: errorDetail?.errorId,
|
||||
});
|
||||
history.push(`/trace/${errorDetail.traceID}?spanId=${errorDetail.spanID}`);
|
||||
|
||||
genericNavigate(
|
||||
`/trace/${errorDetail.traceID}?spanId=${errorDetail.spanID}`,
|
||||
event,
|
||||
);
|
||||
};
|
||||
|
||||
const logEventCalledRef = useRef(false);
|
||||
@@ -185,7 +190,10 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
||||
|
||||
<DashedContainer>
|
||||
<Typography>{t('see_trace_graph')}</Typography>
|
||||
<Button onClick={onClickTraceHandler} type="primary">
|
||||
<Button
|
||||
onClick={(event: React.MouseEvent): void => onClickTraceHandler(event)}
|
||||
type="primary"
|
||||
>
|
||||
{t('see_error_in_trace_graph')}
|
||||
</Button>
|
||||
</DashedContainer>
|
||||
|
||||
@@ -73,6 +73,7 @@ import { ViewProps } from 'types/api/saveViews/types';
|
||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { panelTypeToExplorerView } from 'utils/explorerUtils';
|
||||
import { genericNavigate } from 'utils/genericNavigate';
|
||||
|
||||
import { PreservedViewsTypes } from './constants';
|
||||
import ExplorerOptionsHideArea from './ExplorerOptionsHideArea';
|
||||
@@ -192,7 +193,7 @@ function ExplorerOptions({
|
||||
);
|
||||
|
||||
const onCreateAlertsHandler = useCallback(
|
||||
(defaultQuery: Query | null) => {
|
||||
(defaultQuery: Query | null, event?: React.MouseEvent) => {
|
||||
if (sourcepage === DataSource.TRACES) {
|
||||
logEvent('Traces Explorer: Create alert', {
|
||||
panelType,
|
||||
@@ -211,10 +212,11 @@ function ExplorerOptions({
|
||||
|
||||
const stringifiedQuery = handleConditionalQueryModification(defaultQuery);
|
||||
|
||||
history.push(
|
||||
genericNavigate(
|
||||
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
|
||||
stringifiedQuery,
|
||||
)}`,
|
||||
event,
|
||||
);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -749,7 +751,9 @@ function ExplorerOptions({
|
||||
<Button
|
||||
disabled={disabled}
|
||||
shape="round"
|
||||
onClick={(): void => onCreateAlertsHandler(query)}
|
||||
onClick={(event: React.MouseEvent): void =>
|
||||
onCreateAlertsHandler(query, event)
|
||||
}
|
||||
icon={<ConciergeBell size={16} />}
|
||||
>
|
||||
Create an Alert
|
||||
|
||||
@@ -9,10 +9,11 @@ import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
|
||||
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
|
||||
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { Atom, Play, Terminal } from 'lucide-react';
|
||||
import { Atom, Terminal } from 'lucide-react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
@@ -165,9 +166,8 @@ function QuerySection({
|
||||
onChange={handleQueryCategoryChange}
|
||||
tabBarExtraContent={
|
||||
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={(): void => {
|
||||
<RunQueryBtn
|
||||
onStageRunQuery={(): void => {
|
||||
runQuery();
|
||||
logEvent('Alert: Stage and run query', {
|
||||
dataSource: ALERTS_DATA_SOURCE_MAP[alertType],
|
||||
@@ -176,11 +176,7 @@ function QuerySection({
|
||||
queryType: queryCategory,
|
||||
});
|
||||
}}
|
||||
className="stage-run-query"
|
||||
icon={<Play size={14} />}
|
||||
>
|
||||
Stage & Run Query
|
||||
</Button>
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
items={tabs}
|
||||
@@ -199,14 +195,7 @@ function QuerySection({
|
||||
onChange={handleQueryCategoryChange}
|
||||
tabBarExtraContent={
|
||||
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={runQuery}
|
||||
className="stage-run-query"
|
||||
icon={<Play size={14} />}
|
||||
>
|
||||
Stage & Run Query
|
||||
</Button>
|
||||
<RunQueryBtn onStageRunQuery={runQuery} />
|
||||
</span>
|
||||
}
|
||||
items={items}
|
||||
|
||||
@@ -3,7 +3,6 @@ import getAll from 'api/alerts/getAll';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||
import { ArrowRight, ArrowUpRight, Plus } from 'lucide-react';
|
||||
import Card from 'periscope/components/Card/Card';
|
||||
@@ -13,6 +12,7 @@ import { useQuery } from 'react-query';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { GettableAlert } from 'types/api/alerts/get';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { genericNavigate } from 'utils/genericNavigate';
|
||||
|
||||
export default function AlertRules({
|
||||
onUpdateChecklistDoneItem,
|
||||
@@ -118,7 +118,10 @@ export default function AlertRules({
|
||||
</div>
|
||||
);
|
||||
|
||||
const onEditHandler = (record: GettableAlert) => (): void => {
|
||||
const onEditHandler = (
|
||||
record: GettableAlert,
|
||||
event?: React.MouseEvent | React.KeyboardEvent,
|
||||
): void => {
|
||||
logEvent('Homepage: Alert clicked', {
|
||||
ruleId: record.id,
|
||||
ruleName: record.alert,
|
||||
@@ -135,7 +138,7 @@ export default function AlertRules({
|
||||
|
||||
params.set(QueryParams.ruleId, record.id.toString());
|
||||
|
||||
history.push(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
|
||||
genericNavigate(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`, event);
|
||||
};
|
||||
|
||||
const renderAlertRules = (): JSX.Element => (
|
||||
@@ -147,10 +150,10 @@ export default function AlertRules({
|
||||
tabIndex={0}
|
||||
className="alert-rule-item home-data-item"
|
||||
key={rule.id}
|
||||
onClick={onEditHandler(rule)}
|
||||
onClick={(event: React.MouseEvent): void => onEditHandler(rule, event)}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter') {
|
||||
onEditHandler(rule);
|
||||
onEditHandler(rule, e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { Button, Skeleton, Tag, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ROUTES from 'constants/routes';
|
||||
@@ -9,6 +10,7 @@ import Card from 'periscope/components/Card/Card';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { LicensePlatform } from 'types/api/licensesV3/getActive';
|
||||
import { genericNavigate } from 'utils/genericNavigate';
|
||||
|
||||
import { DOCS_LINKS } from '../constants';
|
||||
|
||||
@@ -84,16 +86,16 @@ function DataSourceInfo({
|
||||
icon={<img src="/Icons/container-plus.svg" alt="plus" />}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={(): void => {
|
||||
onClick={(event: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Connect dataSource clicked', {});
|
||||
|
||||
if (
|
||||
activeLicense &&
|
||||
activeLicense.platform === LicensePlatform.CLOUD
|
||||
) {
|
||||
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
|
||||
genericNavigate(ROUTES.GET_STARTED_WITH_CLOUD, event);
|
||||
} else {
|
||||
window?.open(
|
||||
window.open(
|
||||
DOCS_LINKS.ADD_DATA_SOURCE,
|
||||
'_blank',
|
||||
'noopener noreferrer',
|
||||
|
||||
@@ -30,6 +30,7 @@ import { UserPreference } from 'types/api/preferences/preference';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { isIngestionActive } from 'utils/app';
|
||||
import { genericNavigate } from 'utils/genericNavigate';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import AlertRules from './AlertRules/AlertRules';
|
||||
@@ -550,11 +551,11 @@ export default function Home(): JSX.Element {
|
||||
type="default"
|
||||
className="periscope-btn secondary"
|
||||
icon={<Wrench size={14} />}
|
||||
onClick={(): void => {
|
||||
onClick={(event: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Explore clicked', {
|
||||
source: 'Logs',
|
||||
});
|
||||
history.push(ROUTES.LOGS_EXPLORER);
|
||||
genericNavigate(ROUTES.LOGS_EXPLORER, event);
|
||||
}}
|
||||
>
|
||||
Open Logs Explorer
|
||||
@@ -564,11 +565,11 @@ export default function Home(): JSX.Element {
|
||||
type="default"
|
||||
className="periscope-btn secondary"
|
||||
icon={<Wrench size={14} />}
|
||||
onClick={(): void => {
|
||||
onClick={(event: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Explore clicked', {
|
||||
source: 'Traces',
|
||||
});
|
||||
history.push(ROUTES.TRACES_EXPLORER);
|
||||
genericNavigate(ROUTES.TRACES_EXPLORER, event);
|
||||
}}
|
||||
>
|
||||
Open Traces Explorer
|
||||
@@ -578,11 +579,11 @@ export default function Home(): JSX.Element {
|
||||
type="default"
|
||||
className="periscope-btn secondary"
|
||||
icon={<Wrench size={14} />}
|
||||
onClick={(): void => {
|
||||
onClick={(event: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Explore clicked', {
|
||||
source: 'Metrics',
|
||||
});
|
||||
history.push(ROUTES.METRICS_EXPLORER_EXPLORER);
|
||||
genericNavigate(ROUTES.METRICS_EXPLORER_EXPLORER, event);
|
||||
}}
|
||||
>
|
||||
Open Metrics Explorer
|
||||
@@ -619,11 +620,11 @@ export default function Home(): JSX.Element {
|
||||
type="default"
|
||||
className="periscope-btn secondary"
|
||||
icon={<Plus size={14} />}
|
||||
onClick={(): void => {
|
||||
onClick={(event: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Explore clicked', {
|
||||
source: 'Dashboards',
|
||||
});
|
||||
history.push(ROUTES.ALL_DASHBOARD);
|
||||
genericNavigate(ROUTES.ALL_DASHBOARD, event);
|
||||
}}
|
||||
>
|
||||
Create dashboard
|
||||
@@ -661,11 +662,11 @@ export default function Home(): JSX.Element {
|
||||
type="default"
|
||||
className="periscope-btn secondary"
|
||||
icon={<Plus size={14} />}
|
||||
onClick={(): void => {
|
||||
onClick={(event: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Explore clicked', {
|
||||
source: 'Alerts',
|
||||
});
|
||||
history.push(ROUTES.ALERTS_NEW);
|
||||
genericNavigate(ROUTES.ALERTS_NEW, event);
|
||||
}}
|
||||
>
|
||||
Create an alert
|
||||
|
||||
@@ -4,12 +4,12 @@ import './HomeChecklist.styles.scss';
|
||||
import { Button } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import { ArrowRight, ArrowRightToLine, BookOpenText } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { LicensePlatform } from 'types/api/licensesV3/getActive';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { genericNavigate } from 'utils/genericNavigate';
|
||||
|
||||
export type ChecklistItem = {
|
||||
id: string;
|
||||
@@ -86,18 +86,22 @@ function HomeChecklist({
|
||||
<Button
|
||||
type="default"
|
||||
className="periscope-btn secondary"
|
||||
onClick={(): void => {
|
||||
onClick={(event: React.MouseEvent): void => {
|
||||
logEvent('Welcome Checklist: Get started clicked', {
|
||||
step: item.id,
|
||||
});
|
||||
|
||||
const checkForNewTabAndNavigate = (): void => {
|
||||
genericNavigate(item.toRoute || '', event);
|
||||
};
|
||||
|
||||
if (item.toRoute !== ROUTES.GET_STARTED_WITH_CLOUD) {
|
||||
history.push(item.toRoute || '');
|
||||
checkForNewTabAndNavigate();
|
||||
} else if (
|
||||
activeLicense &&
|
||||
activeLicense.platform === LicensePlatform.CLOUD
|
||||
) {
|
||||
history.push(item.toRoute || '');
|
||||
checkForNewTabAndNavigate();
|
||||
} else {
|
||||
window?.open(
|
||||
item.docsLink || '',
|
||||
|
||||
@@ -11,7 +11,6 @@ import useGetTopLevelOperations from 'hooks/useGetTopLevelOperations';
|
||||
import useResourceAttribute from 'hooks/useResourceAttribute';
|
||||
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import history from 'lib/history';
|
||||
import { ArrowRight, ArrowUpRight } from 'lucide-react';
|
||||
import Card from 'periscope/components/Card/Card';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
@@ -29,6 +28,8 @@ import { ServicesList } from 'types/api/metrics/getService';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { Tags } from 'types/reducer/trace';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { genericNavigate } from 'utils/genericNavigate';
|
||||
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { DOCS_LINKS } from '../constants';
|
||||
@@ -64,7 +65,7 @@ const EmptyState = memo(
|
||||
<Button
|
||||
type="default"
|
||||
className="periscope-btn secondary"
|
||||
onClick={(): void => {
|
||||
onClick={(event: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Get Started clicked', {
|
||||
source: 'Service Metrics',
|
||||
});
|
||||
@@ -73,7 +74,7 @@ const EmptyState = memo(
|
||||
activeLicenseV3 &&
|
||||
activeLicenseV3.platform === LicensePlatform.CLOUD
|
||||
) {
|
||||
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
|
||||
genericNavigate(ROUTES.GET_STARTED_WITH_CLOUD, event);
|
||||
} else {
|
||||
window?.open(
|
||||
DOCS_LINKS.ADD_DATA_SOURCE,
|
||||
@@ -116,7 +117,7 @@ const ServicesListTable = memo(
|
||||
onRowClick,
|
||||
}: {
|
||||
services: ServicesList[];
|
||||
onRowClick: (record: ServicesList) => void;
|
||||
onRowClick: (record: ServicesList, event: React.MouseEvent) => void;
|
||||
}): JSX.Element => (
|
||||
<div className="services-list-container home-data-item-container metrics-services-list">
|
||||
<div className="services-list">
|
||||
@@ -125,8 +126,8 @@ const ServicesListTable = memo(
|
||||
dataSource={services}
|
||||
pagination={false}
|
||||
className="services-table"
|
||||
onRow={(record): { onClick: () => void } => ({
|
||||
onClick: (): void => onRowClick(record),
|
||||
onRow={(record): { onClick: (event: React.MouseEvent) => void } => ({
|
||||
onClick: (event: React.MouseEvent): void => onRowClick(record, event),
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
@@ -284,11 +285,19 @@ function ServiceMetrics({
|
||||
}, [onUpdateChecklistDoneItem, loadingUserPreferences, servicesExist]);
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
(record: ServicesList) => {
|
||||
(record: ServicesList, event: React.MouseEvent) => {
|
||||
logEvent('Homepage: Service clicked', {
|
||||
serviceName: record.serviceName,
|
||||
});
|
||||
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`);
|
||||
if (event && isCtrlOrMMetaKey(event)) {
|
||||
window.open(
|
||||
`${ROUTES.APPLICATION}/${record.serviceName}`,
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
);
|
||||
} else {
|
||||
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`);
|
||||
}
|
||||
},
|
||||
[safeNavigate],
|
||||
);
|
||||
|
||||
@@ -3,7 +3,6 @@ import logEvent from 'api/common/logEvent';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useQueryService } from 'hooks/useQueryService';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import history from 'lib/history';
|
||||
import { ArrowRight, ArrowUpRight } from 'lucide-react';
|
||||
import Card from 'periscope/components/Card/Card';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
@@ -15,6 +14,8 @@ import { LicensePlatform } from 'types/api/licensesV3/getActive';
|
||||
import { ServicesList } from 'types/api/metrics/getService';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { genericNavigate } from 'utils/genericNavigate';
|
||||
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
|
||||
|
||||
import { DOCS_LINKS } from '../constants';
|
||||
import { columns, TIME_PICKER_OPTIONS } from './constants';
|
||||
@@ -118,7 +119,7 @@ export default function ServiceTraces({
|
||||
<Button
|
||||
type="default"
|
||||
className="periscope-btn secondary"
|
||||
onClick={(): void => {
|
||||
onClick={(event: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Get Started clicked', {
|
||||
source: 'Service Traces',
|
||||
});
|
||||
@@ -127,7 +128,7 @@ export default function ServiceTraces({
|
||||
activeLicense &&
|
||||
activeLicense.platform === LicensePlatform.CLOUD
|
||||
) {
|
||||
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
|
||||
genericNavigate(ROUTES.GET_STARTED_WITH_CLOUD, event);
|
||||
} else {
|
||||
window?.open(
|
||||
DOCS_LINKS.ADD_DATA_SOURCE,
|
||||
@@ -172,13 +173,21 @@ export default function ServiceTraces({
|
||||
dataSource={top5Services}
|
||||
pagination={false}
|
||||
className="services-table"
|
||||
onRow={(record): { onClick: () => void } => ({
|
||||
onClick: (): void => {
|
||||
onRow={(record): { onClick: (event: React.MouseEvent) => void } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Service clicked', {
|
||||
serviceName: record.serviceName,
|
||||
});
|
||||
|
||||
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`);
|
||||
if (event && isCtrlOrMMetaKey(event)) {
|
||||
window.open(
|
||||
`${ROUTES.APPLICATION}/${record.serviceName}`,
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
);
|
||||
} else {
|
||||
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`);
|
||||
}
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -5,10 +5,10 @@ import { Button, Divider, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ROUTES from 'constants/routes';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import history from 'lib/history';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { genericNavigate } from 'utils/genericNavigate';
|
||||
|
||||
import AlertInfoCard from './AlertInfoCard';
|
||||
import { ALERT_CARDS, ALERT_INFO_LINKS } from './alertLinks';
|
||||
@@ -36,9 +36,9 @@ export function AlertsEmptyState(): JSX.Element {
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const onClickNewAlertHandler = useCallback(() => {
|
||||
const onClickNewAlertHandler = useCallback((event: React.MouseEvent): void => {
|
||||
setLoading(false);
|
||||
history.push(ROUTES.ALERTS_NEW);
|
||||
genericNavigate(ROUTES.ALERTS_NEW, event);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -70,7 +70,9 @@ export function AlertsEmptyState(): JSX.Element {
|
||||
<div className="action-container">
|
||||
<Button
|
||||
className="add-alert-btn"
|
||||
onClick={onClickNewAlertHandler}
|
||||
onClick={(event: React.MouseEvent): void =>
|
||||
onClickNewAlertHandler(event)
|
||||
}
|
||||
icon={<PlusOutlined />}
|
||||
disabled={!addNewAlert}
|
||||
loading={loading}
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
Flex,
|
||||
Input,
|
||||
MenuProps,
|
||||
Tag,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { Button, Flex, Input, Typography } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table/interface';
|
||||
import saveAlertApi from 'api/alerts/save';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
@@ -39,6 +31,7 @@ import { UseQueryResult } from 'react-query';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { GettableAlert } from 'types/api/alerts/get';
|
||||
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
|
||||
|
||||
import DeleteAlert from './DeleteAlert';
|
||||
import { ColumnButton, SearchContainer } from './styles';
|
||||
@@ -274,7 +267,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
const onClickHandler = (e: React.MouseEvent<HTMLElement>): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onEditHandler(record, e.metaKey || e.ctrlKey);
|
||||
onEditHandler(record, isCtrlOrMMetaKey(e));
|
||||
};
|
||||
|
||||
return <Typography.Link onClick={onClickHandler}>{value}</Typography.Link>;
|
||||
|
||||
@@ -118,7 +118,7 @@ const templatesList: DashboardTemplate[] = [
|
||||
|
||||
interface DashboardTemplatesModalProps {
|
||||
showNewDashboardTemplatesModal: boolean;
|
||||
onCreateNewDashboard: () => void;
|
||||
onCreateNewDashboard: (event: React.MouseEvent) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
@@ -204,7 +204,9 @@ export default function DashboardTemplatesModal({
|
||||
type="primary"
|
||||
className="periscope-btn primary"
|
||||
icon={<Plus size={14} />}
|
||||
onClick={onCreateNewDashboard}
|
||||
onClick={(event: React.MouseEvent): void =>
|
||||
onCreateNewDashboard(event)
|
||||
}
|
||||
>
|
||||
New dashboard
|
||||
</Button>
|
||||
|
||||
@@ -86,6 +86,7 @@ import {
|
||||
Widgets,
|
||||
} from 'types/api/dashboard/getAll';
|
||||
import APIError from 'types/api/error';
|
||||
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
|
||||
|
||||
import DashboardTemplatesModal from './DashboardTemplates/DashboardTemplatesModal';
|
||||
import ImportJSON from './ImportJSON';
|
||||
@@ -284,35 +285,48 @@ function DashboardsList(): JSX.Element {
|
||||
refetchDashboardList,
|
||||
})) || [];
|
||||
|
||||
const onNewDashboardHandler = useCallback(async () => {
|
||||
try {
|
||||
logEvent('Dashboard List: Create dashboard clicked', {});
|
||||
setNewDashboardState({
|
||||
...newDashboardState,
|
||||
loading: true,
|
||||
});
|
||||
const response = await createDashboard({
|
||||
title: t('new_dashboard_title', {
|
||||
ns: 'dashboard',
|
||||
}),
|
||||
uploadedGrafana: false,
|
||||
version: ENTITY_VERSION_V5,
|
||||
});
|
||||
const onNewDashboardHandler = useCallback(
|
||||
async (event: React.MouseEvent): Promise<void> => {
|
||||
try {
|
||||
logEvent('Dashboard List: Create dashboard clicked', {});
|
||||
setNewDashboardState({
|
||||
...newDashboardState,
|
||||
loading: true,
|
||||
});
|
||||
const response = await createDashboard({
|
||||
title: t('new_dashboard_title', {
|
||||
ns: 'dashboard',
|
||||
}),
|
||||
uploadedGrafana: false,
|
||||
version: ENTITY_VERSION_V5,
|
||||
});
|
||||
|
||||
safeNavigate(
|
||||
generatePath(ROUTES.DASHBOARD, {
|
||||
dashboardId: response.data.id,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
setNewDashboardState({
|
||||
...newDashboardState,
|
||||
error: true,
|
||||
errorMessage: (error as AxiosError).toString() || 'Something went Wrong',
|
||||
});
|
||||
}
|
||||
}, [newDashboardState, safeNavigate, showErrorModal, t]);
|
||||
if (event && isCtrlOrMMetaKey(event)) {
|
||||
window.open(
|
||||
generatePath(ROUTES.DASHBOARD, {
|
||||
dashboardId: response.data.id,
|
||||
}),
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
);
|
||||
} else {
|
||||
safeNavigate(
|
||||
generatePath(ROUTES.DASHBOARD, {
|
||||
dashboardId: response.data.id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
setNewDashboardState({
|
||||
...newDashboardState,
|
||||
error: true,
|
||||
errorMessage: (error as AxiosError).toString() || 'Something went Wrong',
|
||||
});
|
||||
}
|
||||
},
|
||||
[newDashboardState, safeNavigate, showErrorModal, t],
|
||||
);
|
||||
|
||||
const onModalHandler = (uploadedGrafana: boolean): void => {
|
||||
logEvent('Dashboard List: Import JSON clicked', {});
|
||||
@@ -414,8 +428,8 @@ function DashboardsList(): JSX.Element {
|
||||
|
||||
const onClickHandler = (event: React.MouseEvent<HTMLElement>): void => {
|
||||
event.stopPropagation();
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
window.open(getLink(), '_blank');
|
||||
if (isCtrlOrMMetaKey(event)) {
|
||||
window.open(getLink(), '_blank', 'noopener,noreferrer');
|
||||
} else {
|
||||
safeNavigate(getLink());
|
||||
}
|
||||
@@ -641,8 +655,8 @@ function DashboardsList(): JSX.Element {
|
||||
label: (
|
||||
<div
|
||||
className="create-dashboard-menu-item"
|
||||
onClick={(): void => {
|
||||
onNewDashboardHandler();
|
||||
onClick={(event: React.MouseEvent): void => {
|
||||
onNewDashboardHandler(event);
|
||||
}}
|
||||
>
|
||||
<LayoutGrid size={14} /> Create dashboard
|
||||
@@ -929,7 +943,9 @@ function DashboardsList(): JSX.Element {
|
||||
|
||||
<DashboardTemplatesModal
|
||||
showNewDashboardTemplatesModal={showNewDashboardTemplatesModal}
|
||||
onCreateNewDashboard={onNewDashboardHandler}
|
||||
onCreateNewDashboard={(event: React.MouseEvent): Promise<void> =>
|
||||
onNewDashboardHandler(event)
|
||||
}
|
||||
onCancel={(): void => {
|
||||
setShowNewDashboardTemplatesModal(false);
|
||||
}}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LockFilled } from '@ant-design/icons';
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import { genericNavigate } from 'utils/genericNavigate';
|
||||
|
||||
import { Data } from '../DashboardsList';
|
||||
import { TableLinkText } from './styles';
|
||||
@@ -11,11 +11,7 @@ function Name(name: Data['name'], data: Data): JSX.Element {
|
||||
const getLink = (): string => `${ROUTES.ALL_DASHBOARD}/${DashboardId}`;
|
||||
|
||||
const onClickHandler = (event: React.MouseEvent<HTMLElement>): void => {
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
window.open(getLink(), '_blank');
|
||||
} else {
|
||||
history.push(getLink());
|
||||
}
|
||||
genericNavigate(getLink(), event);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
import { Col } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
@@ -179,14 +180,17 @@ function DBCall(): JSX.Element {
|
||||
type="default"
|
||||
size="small"
|
||||
id="database_call_rps_button"
|
||||
onClick={onViewTracePopupClick({
|
||||
servicename,
|
||||
selectedTraceTags,
|
||||
timestamp: selectedTimeStamp,
|
||||
apmToTraceQuery,
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
})}
|
||||
onClick={(event): void =>
|
||||
onViewTracePopupClick({
|
||||
servicename,
|
||||
selectedTraceTags,
|
||||
timestamp: selectedTimeStamp,
|
||||
apmToTraceQuery,
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
event,
|
||||
})
|
||||
}
|
||||
>
|
||||
View Traces
|
||||
</Button>
|
||||
@@ -215,14 +219,17 @@ function DBCall(): JSX.Element {
|
||||
type="default"
|
||||
size="small"
|
||||
id="database_call_avg_duration_button"
|
||||
onClick={onViewTracePopupClick({
|
||||
servicename,
|
||||
selectedTraceTags,
|
||||
timestamp: selectedTimeStamp,
|
||||
apmToTraceQuery,
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
})}
|
||||
onClick={(event): void =>
|
||||
onViewTracePopupClick({
|
||||
servicename,
|
||||
selectedTraceTags,
|
||||
timestamp: selectedTimeStamp,
|
||||
apmToTraceQuery,
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
event,
|
||||
})
|
||||
}
|
||||
>
|
||||
View Traces
|
||||
</Button>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
import { Col } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
@@ -244,22 +245,28 @@ function External(): JSX.Element {
|
||||
<Col span={12}>
|
||||
<GraphControlsPanel
|
||||
id="external_call_error_percentage_button"
|
||||
onViewTracesClick={onViewTracePopupClick({
|
||||
servicename,
|
||||
selectedTraceTags,
|
||||
timestamp: selectedTimeStamp,
|
||||
apmToTraceQuery: errorApmToTraceQuery,
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
})}
|
||||
onViewAPIMonitoringClick={onViewAPIMonitoringPopupClick({
|
||||
servicename,
|
||||
timestamp: selectedTimeStamp,
|
||||
domainName: selectedData?.address || '',
|
||||
isError: true,
|
||||
stepInterval: 300,
|
||||
safeNavigate,
|
||||
})}
|
||||
onViewTracesClick={(event: React.MouseEvent): void =>
|
||||
onViewTracePopupClick({
|
||||
servicename,
|
||||
selectedTraceTags,
|
||||
timestamp: selectedTimeStamp,
|
||||
apmToTraceQuery: errorApmToTraceQuery,
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
event,
|
||||
})
|
||||
}
|
||||
onViewAPIMonitoringClick={(event: React.MouseEvent): void =>
|
||||
onViewAPIMonitoringPopupClick({
|
||||
servicename,
|
||||
timestamp: selectedTimeStamp,
|
||||
domainName: selectedData?.address || '',
|
||||
isError: true,
|
||||
stepInterval: 300,
|
||||
safeNavigate,
|
||||
event,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Card data-testid="external_call_error_percentage">
|
||||
<GraphContainer>
|
||||
@@ -286,22 +293,28 @@ function External(): JSX.Element {
|
||||
<Col span={12}>
|
||||
<GraphControlsPanel
|
||||
id="external_call_duration_button"
|
||||
onViewTracesClick={onViewTracePopupClick({
|
||||
servicename,
|
||||
selectedTraceTags,
|
||||
timestamp: selectedTimeStamp,
|
||||
apmToTraceQuery,
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
})}
|
||||
onViewAPIMonitoringClick={onViewAPIMonitoringPopupClick({
|
||||
servicename,
|
||||
timestamp: selectedTimeStamp,
|
||||
domainName: selectedData?.address,
|
||||
isError: false,
|
||||
stepInterval: 300,
|
||||
safeNavigate,
|
||||
})}
|
||||
onViewTracesClick={(event: React.MouseEvent): void =>
|
||||
onViewTracePopupClick({
|
||||
servicename,
|
||||
selectedTraceTags,
|
||||
timestamp: selectedTimeStamp,
|
||||
apmToTraceQuery,
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
event,
|
||||
})
|
||||
}
|
||||
onViewAPIMonitoringClick={(event: React.MouseEvent): void =>
|
||||
onViewAPIMonitoringPopupClick({
|
||||
servicename,
|
||||
timestamp: selectedTimeStamp,
|
||||
domainName: selectedData?.address,
|
||||
isError: false,
|
||||
stepInterval: 300,
|
||||
safeNavigate,
|
||||
event,
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
<Card data-testid="external_call_duration">
|
||||
@@ -331,22 +344,28 @@ function External(): JSX.Element {
|
||||
<Col span={12}>
|
||||
<GraphControlsPanel
|
||||
id="external_call_rps_by_address_button"
|
||||
onViewTracesClick={onViewTracePopupClick({
|
||||
servicename,
|
||||
selectedTraceTags,
|
||||
timestamp: selectedTimeStamp,
|
||||
apmToTraceQuery,
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
})}
|
||||
onViewAPIMonitoringClick={onViewAPIMonitoringPopupClick({
|
||||
servicename,
|
||||
timestamp: selectedTimeStamp,
|
||||
domainName: selectedData?.address,
|
||||
isError: false,
|
||||
stepInterval: 300,
|
||||
safeNavigate,
|
||||
})}
|
||||
onViewTracesClick={(event: React.MouseEvent): void =>
|
||||
onViewTracePopupClick({
|
||||
servicename,
|
||||
selectedTraceTags,
|
||||
timestamp: selectedTimeStamp,
|
||||
apmToTraceQuery,
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
event,
|
||||
})
|
||||
}
|
||||
onViewAPIMonitoringClick={(event: React.MouseEvent): void =>
|
||||
onViewAPIMonitoringPopupClick({
|
||||
servicename,
|
||||
timestamp: selectedTimeStamp,
|
||||
domainName: selectedData?.address,
|
||||
isError: false,
|
||||
stepInterval: 300,
|
||||
safeNavigate,
|
||||
event,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Card data-testid="external_call_rps_by_address">
|
||||
<GraphContainer>
|
||||
@@ -373,22 +392,28 @@ function External(): JSX.Element {
|
||||
<Col span={12}>
|
||||
<GraphControlsPanel
|
||||
id="external_call_duration_by_address_button"
|
||||
onViewTracesClick={onViewTracePopupClick({
|
||||
servicename,
|
||||
selectedTraceTags,
|
||||
timestamp: selectedTimeStamp,
|
||||
apmToTraceQuery,
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
})}
|
||||
onViewAPIMonitoringClick={onViewAPIMonitoringPopupClick({
|
||||
servicename,
|
||||
timestamp: selectedTimeStamp,
|
||||
domainName: selectedData?.address,
|
||||
isError: false,
|
||||
stepInterval: 300,
|
||||
safeNavigate,
|
||||
})}
|
||||
onViewTracesClick={(event: React.MouseEvent): void =>
|
||||
onViewTracePopupClick({
|
||||
servicename,
|
||||
selectedTraceTags,
|
||||
timestamp: selectedTimeStamp,
|
||||
apmToTraceQuery,
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
event,
|
||||
})
|
||||
}
|
||||
onViewAPIMonitoringClick={(event: React.MouseEvent): void =>
|
||||
onViewAPIMonitoringPopupClick({
|
||||
servicename,
|
||||
timestamp: selectedTimeStamp,
|
||||
domainName: selectedData?.address,
|
||||
isError: false,
|
||||
stepInterval: 300,
|
||||
safeNavigate,
|
||||
event,
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
<Card data-testid="external_call_duration_by_address">
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import getTopLevelOperations, {
|
||||
ServiceDataProps,
|
||||
@@ -31,6 +32,7 @@ import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { genericNavigate } from 'utils/genericNavigate';
|
||||
import { secondsToMilliseconds } from 'utils/timeUtils';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
@@ -228,14 +230,16 @@ function Application(): JSX.Element {
|
||||
* @param timestamp - The timestamp in seconds
|
||||
* @param apmToTraceQuery - query object
|
||||
* @param isViewLogsClicked - Whether this is for viewing logs vs traces
|
||||
* @param event - Click event to handle opening in new tab
|
||||
* @returns A callback function that handles the navigation when executed
|
||||
*/
|
||||
const onErrorTrackHandler = useCallback(
|
||||
(
|
||||
timestamp: number,
|
||||
apmToTraceQuery: Query,
|
||||
event: React.MouseEvent,
|
||||
isViewLogsClicked?: boolean,
|
||||
): (() => void) => (): void => {
|
||||
): void => {
|
||||
const endTime = secondsToMilliseconds(timestamp);
|
||||
const startTime = secondsToMilliseconds(timestamp - stepInterval);
|
||||
|
||||
@@ -259,7 +263,7 @@ function Application(): JSX.Element {
|
||||
queryString,
|
||||
);
|
||||
|
||||
history.push(newPath);
|
||||
genericNavigate(newPath, event);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[stepInterval],
|
||||
@@ -319,14 +323,17 @@ function Application(): JSX.Element {
|
||||
type="default"
|
||||
size="small"
|
||||
id="Rate_button"
|
||||
onClick={onViewTracePopupClick({
|
||||
servicename,
|
||||
selectedTraceTags,
|
||||
timestamp: selectedTimeStamp,
|
||||
apmToTraceQuery,
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
})}
|
||||
onClick={(event: React.MouseEvent): void =>
|
||||
onViewTracePopupClick({
|
||||
servicename,
|
||||
selectedTraceTags,
|
||||
timestamp: selectedTimeStamp,
|
||||
apmToTraceQuery,
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
event,
|
||||
})
|
||||
}
|
||||
>
|
||||
View Traces
|
||||
</Button>
|
||||
@@ -349,14 +356,17 @@ function Application(): JSX.Element {
|
||||
type="default"
|
||||
size="small"
|
||||
id="ApDex_button"
|
||||
onClick={onViewTracePopupClick({
|
||||
servicename,
|
||||
selectedTraceTags,
|
||||
timestamp: selectedTimeStamp,
|
||||
apmToTraceQuery,
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
})}
|
||||
onClick={(event: React.MouseEvent): void =>
|
||||
onViewTracePopupClick({
|
||||
servicename,
|
||||
selectedTraceTags,
|
||||
timestamp: selectedTimeStamp,
|
||||
apmToTraceQuery,
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
event,
|
||||
})
|
||||
}
|
||||
>
|
||||
View Traces
|
||||
</Button>
|
||||
@@ -370,15 +380,12 @@ function Application(): JSX.Element {
|
||||
<ColErrorContainer>
|
||||
<GraphControlsPanel
|
||||
id="Error_button"
|
||||
onViewLogsClick={onErrorTrackHandler(
|
||||
selectedTimeStamp,
|
||||
logErrorQuery,
|
||||
true,
|
||||
)}
|
||||
onViewTracesClick={onErrorTrackHandler(
|
||||
selectedTimeStamp,
|
||||
errorTrackQuery,
|
||||
)}
|
||||
onViewLogsClick={(event: React.MouseEvent): void =>
|
||||
onErrorTrackHandler(selectedTimeStamp, logErrorQuery, event, true)
|
||||
}
|
||||
onViewTracesClick={(event: React.MouseEvent): void =>
|
||||
onErrorTrackHandler(selectedTimeStamp, errorTrackQuery, event)
|
||||
}
|
||||
/>
|
||||
|
||||
<TopLevelOperation
|
||||
|
||||
@@ -6,9 +6,9 @@ import { Binoculars, DraftingCompass, ScrollText } from 'lucide-react';
|
||||
|
||||
interface GraphControlsPanelProps {
|
||||
id: string;
|
||||
onViewLogsClick?: () => void;
|
||||
onViewTracesClick: () => void;
|
||||
onViewAPIMonitoringClick?: () => void;
|
||||
onViewLogsClick?: (event: React.MouseEvent) => void;
|
||||
onViewTracesClick: (event: React.MouseEvent) => void;
|
||||
onViewAPIMonitoringClick?: (event: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
function GraphControlsPanel({
|
||||
@@ -23,7 +23,7 @@ function GraphControlsPanel({
|
||||
type="link"
|
||||
icon={<DraftingCompass size={14} />}
|
||||
size="small"
|
||||
onClick={onViewTracesClick}
|
||||
onClick={(event: React.MouseEvent): void => onViewTracesClick(event)}
|
||||
style={{ color: Color.BG_VANILLA_100 }}
|
||||
>
|
||||
View traces
|
||||
@@ -33,7 +33,7 @@ function GraphControlsPanel({
|
||||
type="link"
|
||||
icon={<ScrollText size={14} />}
|
||||
size="small"
|
||||
onClick={onViewLogsClick}
|
||||
onClick={(event: React.MouseEvent): void => onViewLogsClick(event)}
|
||||
style={{ color: Color.BG_VANILLA_100 }}
|
||||
>
|
||||
View logs
|
||||
@@ -44,7 +44,9 @@ function GraphControlsPanel({
|
||||
type="link"
|
||||
icon={<Binoculars size={14} />}
|
||||
size="small"
|
||||
onClick={onViewAPIMonitoringClick}
|
||||
onClick={(event: React.MouseEvent): void =>
|
||||
onViewAPIMonitoringClick(event)
|
||||
}
|
||||
style={{ color: Color.BG_VANILLA_100 }}
|
||||
>
|
||||
View External APIs
|
||||
|
||||
@@ -103,23 +103,29 @@ function ServiceOverview({
|
||||
<>
|
||||
<GraphControlsPanel
|
||||
id="Service_button"
|
||||
onViewLogsClick={onViewTracePopupClick({
|
||||
servicename,
|
||||
selectedTraceTags,
|
||||
timestamp: selectedTimeStamp,
|
||||
apmToTraceQuery: apmToLogQuery,
|
||||
isViewLogsClicked: true,
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
})}
|
||||
onViewTracesClick={onViewTracePopupClick({
|
||||
servicename,
|
||||
selectedTraceTags,
|
||||
timestamp: selectedTimeStamp,
|
||||
apmToTraceQuery,
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
})}
|
||||
onViewLogsClick={(event: React.MouseEvent): void =>
|
||||
onViewTracePopupClick({
|
||||
servicename,
|
||||
selectedTraceTags,
|
||||
timestamp: selectedTimeStamp,
|
||||
apmToTraceQuery: apmToLogQuery,
|
||||
isViewLogsClicked: true,
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
event,
|
||||
})
|
||||
}
|
||||
onViewTracesClick={(event: React.MouseEvent): void =>
|
||||
onViewTracePopupClick({
|
||||
servicename,
|
||||
selectedTraceTags,
|
||||
timestamp: selectedTimeStamp,
|
||||
apmToTraceQuery,
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
event,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Card data-testid="service_latency">
|
||||
<GraphContainer>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { navigateToTrace } from 'container/MetricsApplication/utils';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { useGetAPMToTracesQueries } from '../../util';
|
||||
@@ -50,7 +51,7 @@ function ColumnWithLink({
|
||||
return (
|
||||
<Tooltip placement="topLeft" title={text}>
|
||||
<Typography.Link
|
||||
onClick={(e): void => handleOnClick(text, e.metaKey || e.ctrlKey)}
|
||||
onClick={(e): void => handleOnClick(text, isCtrlOrMMetaKey(e))}
|
||||
>
|
||||
{text}
|
||||
</Typography.Link>
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { Tags } from 'types/reducer/trace';
|
||||
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
|
||||
import { secondsToMilliseconds } from 'utils/timeUtils';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
@@ -43,6 +44,7 @@ interface OnViewTracePopupClickProps {
|
||||
isViewLogsClicked?: boolean;
|
||||
stepInterval?: number;
|
||||
safeNavigate: (url: string) => void;
|
||||
event: React.MouseEvent;
|
||||
}
|
||||
|
||||
interface OnViewAPIMonitoringPopupClickProps {
|
||||
@@ -53,6 +55,7 @@ interface OnViewAPIMonitoringPopupClickProps {
|
||||
isError: boolean;
|
||||
|
||||
safeNavigate: (url: string) => void;
|
||||
event: React.MouseEvent;
|
||||
}
|
||||
|
||||
export function generateExplorerPath(
|
||||
@@ -83,7 +86,7 @@ export function generateExplorerPath(
|
||||
* @param isViewLogsClicked - Whether this is for viewing logs vs traces
|
||||
* @param stepInterval - Time interval in seconds
|
||||
* @param safeNavigate - Navigation function
|
||||
|
||||
* @param event - Click event to handle opening in new tab
|
||||
*/
|
||||
export function onViewTracePopupClick({
|
||||
selectedTraceTags,
|
||||
@@ -93,33 +96,34 @@ export function onViewTracePopupClick({
|
||||
isViewLogsClicked,
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
}: OnViewTracePopupClickProps): VoidFunction {
|
||||
return (): void => {
|
||||
const endTime = secondsToMilliseconds(timestamp);
|
||||
const startTime = secondsToMilliseconds(timestamp - (stepInterval || 60));
|
||||
event,
|
||||
}: OnViewTracePopupClickProps): void {
|
||||
const endTime = secondsToMilliseconds(timestamp);
|
||||
const startTime = secondsToMilliseconds(timestamp - (stepInterval || 60));
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
urlParams.set(QueryParams.startTime, startTime.toString());
|
||||
urlParams.set(QueryParams.endTime, endTime.toString());
|
||||
urlParams.delete(QueryParams.relativeTime);
|
||||
const avialableParams = routeConfig[ROUTES.TRACE];
|
||||
const queryString = getQueryString(avialableParams, urlParams);
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
urlParams.set(QueryParams.startTime, startTime.toString());
|
||||
urlParams.set(QueryParams.endTime, endTime.toString());
|
||||
urlParams.delete(QueryParams.relativeTime);
|
||||
const avialableParams = routeConfig[ROUTES.TRACE];
|
||||
const queryString = getQueryString(avialableParams, urlParams);
|
||||
|
||||
const JSONCompositeQuery = encodeURIComponent(
|
||||
JSON.stringify(apmToTraceQuery),
|
||||
);
|
||||
const JSONCompositeQuery = encodeURIComponent(JSON.stringify(apmToTraceQuery));
|
||||
|
||||
const newPath = generateExplorerPath(
|
||||
isViewLogsClicked,
|
||||
urlParams,
|
||||
servicename,
|
||||
selectedTraceTags,
|
||||
JSONCompositeQuery,
|
||||
queryString,
|
||||
);
|
||||
const newPath = generateExplorerPath(
|
||||
isViewLogsClicked,
|
||||
urlParams,
|
||||
servicename,
|
||||
selectedTraceTags,
|
||||
JSONCompositeQuery,
|
||||
queryString,
|
||||
);
|
||||
|
||||
if (event && isCtrlOrMMetaKey(event)) {
|
||||
window.open(newPath, '_blank', 'noopener,noreferrer');
|
||||
} else {
|
||||
safeNavigate(newPath);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const generateAPIMonitoringPath = (
|
||||
@@ -149,49 +153,52 @@ export function onViewAPIMonitoringPopupClick({
|
||||
isError,
|
||||
stepInterval,
|
||||
safeNavigate,
|
||||
}: OnViewAPIMonitoringPopupClickProps): VoidFunction {
|
||||
return (): void => {
|
||||
const endTime = timestamp + (stepInterval || 60);
|
||||
const startTime = timestamp - (stepInterval || 60);
|
||||
const filters = {
|
||||
items: [
|
||||
...(isError
|
||||
? [
|
||||
{
|
||||
id: uuid().slice(0, 8),
|
||||
key: {
|
||||
key: 'hasError',
|
||||
dataType: DataTypes.bool,
|
||||
type: 'tag',
|
||||
id: 'hasError--bool--tag--true',
|
||||
},
|
||||
op: 'in',
|
||||
value: ['true'],
|
||||
event,
|
||||
}: OnViewAPIMonitoringPopupClickProps): void {
|
||||
const endTime = timestamp + (stepInterval || 60);
|
||||
const startTime = timestamp - (stepInterval || 60);
|
||||
const filters = {
|
||||
items: [
|
||||
...(isError
|
||||
? [
|
||||
{
|
||||
id: uuid().slice(0, 8),
|
||||
key: {
|
||||
key: 'hasError',
|
||||
dataType: DataTypes.bool,
|
||||
type: 'tag',
|
||||
id: 'hasError--bool--tag--true',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: uuid().slice(0, 8),
|
||||
key: {
|
||||
key: 'service.name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
},
|
||||
op: '=',
|
||||
value: servicename,
|
||||
op: 'in',
|
||||
value: ['true'],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: uuid().slice(0, 8),
|
||||
key: {
|
||||
key: 'service.name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
const newPath = generateAPIMonitoringPath(
|
||||
domainName,
|
||||
startTime,
|
||||
endTime,
|
||||
filters,
|
||||
);
|
||||
|
||||
safeNavigate(newPath);
|
||||
op: '=',
|
||||
value: servicename,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
const newPath = generateAPIMonitoringPath(
|
||||
domainName,
|
||||
startTime,
|
||||
endTime,
|
||||
filters,
|
||||
);
|
||||
|
||||
if (event && isCtrlOrMMetaKey(event)) {
|
||||
window.open(newPath, '_blank', 'noopener,noreferrer');
|
||||
} else {
|
||||
safeNavigate(newPath);
|
||||
}
|
||||
}
|
||||
|
||||
export function useGraphClickHandler(
|
||||
|
||||
@@ -17,6 +17,7 @@ import { AppState } from 'store/reducers';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { IServiceName } from './Tabs/types';
|
||||
@@ -115,7 +116,7 @@ function TopOperationsTable({
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
if (isCtrlOrMMetaKey(e)) {
|
||||
handleOnClick(text, true); // open in new tab
|
||||
} else {
|
||||
handleOnClick(text, false); // open in current tab
|
||||
|
||||
@@ -91,7 +91,7 @@ function Summary(): JSX.Element {
|
||||
const queryFiltersWithoutId = useMemo(() => {
|
||||
const filtersWithoutId = {
|
||||
...queryFilters,
|
||||
items: queryFilters.items.map(({ id, ...rest }) => rest),
|
||||
items: queryFilters.items.map(({ id: _id, ...rest }) => rest),
|
||||
};
|
||||
return JSON.stringify(filtersWithoutId);
|
||||
}, [queryFilters]);
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
getDefaultWidgetData,
|
||||
PANEL_TYPE_TO_QUERY_TYPES,
|
||||
} from 'container/NewWidget/utils';
|
||||
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
|
||||
// import { QueryBuilder } from 'container/QueryBuilder';
|
||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||
@@ -20,7 +21,7 @@ import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { defaultTo, isUndefined } from 'lodash-es';
|
||||
import { Atom, Play, Terminal } from 'lucide-react';
|
||||
import { Atom, Terminal } from 'lucide-react';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import {
|
||||
getNextWidgets,
|
||||
@@ -28,20 +29,14 @@ import {
|
||||
getSelectedWidgetIndex,
|
||||
} from 'providers/Dashboard/util';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import ClickHouseQueryContainer from './QueryBuilder/clickHouse';
|
||||
import PromQLQueryContainer from './QueryBuilder/promQL';
|
||||
|
||||
function QuerySection({
|
||||
selectedGraph,
|
||||
queryResponse,
|
||||
}: QueryProps): JSX.Element {
|
||||
function QuerySection({ selectedGraph }: QueryProps): JSX.Element {
|
||||
const {
|
||||
currentQuery,
|
||||
handleRunQuery: handleRunQueryFromQueryBuilder,
|
||||
@@ -242,15 +237,7 @@ function QuerySection({
|
||||
tabBarExtraContent={
|
||||
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
||||
<TextToolTip text="This will temporarily save the current query and graph state. This will persist across tab change" />
|
||||
<Button
|
||||
loading={queryResponse.isFetching}
|
||||
type="primary"
|
||||
onClick={handleRunQuery}
|
||||
className="stage-run-query"
|
||||
icon={<Play size={14} />}
|
||||
>
|
||||
Stage & Run Query
|
||||
</Button>
|
||||
<RunQueryBtn label="Stage & Run Query" onStageRunQuery={handleRunQuery} />
|
||||
</span>
|
||||
}
|
||||
items={items}
|
||||
@@ -261,10 +248,6 @@ function QuerySection({
|
||||
|
||||
interface QueryProps {
|
||||
selectedGraph: PANEL_TYPES;
|
||||
queryResponse: UseQueryResult<
|
||||
SuccessResponse<MetricRangePayloadProps, unknown>,
|
||||
Error
|
||||
>;
|
||||
}
|
||||
|
||||
export default QuerySection;
|
||||
|
||||
@@ -64,7 +64,7 @@ function LeftContainer({
|
||||
enableDrillDown={enableDrillDown}
|
||||
/>
|
||||
<QueryContainer className="query-section-left-container">
|
||||
<QuerySection selectedGraph={selectedGraph} queryResponse={queryResponse} />
|
||||
<QuerySection selectedGraph={selectedGraph} />
|
||||
{selectedGraph === PANEL_TYPES.LIST && (
|
||||
<ExplorerColumnsRenderer
|
||||
selectedLogFields={selectedLogFields}
|
||||
|
||||
@@ -166,7 +166,7 @@ function UpdateContextLinks({
|
||||
onSave(newContextLink);
|
||||
} catch (error) {
|
||||
// Form validation failed, don't call onSave
|
||||
console.log('Form validation failed:', error);
|
||||
console.error('Form validation failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import './NoLogs.styles.scss';
|
||||
|
||||
import { Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import history from 'lib/history';
|
||||
import { ArrowUpRight } from 'lucide-react';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import DOCLINKS from 'utils/docLinks';
|
||||
import { genericNavigate } from 'utils/genericNavigate';
|
||||
|
||||
export default function NoLogs({
|
||||
dataSource,
|
||||
@@ -16,6 +17,8 @@ export default function NoLogs({
|
||||
}): JSX.Element {
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
|
||||
const REL_NOOPENER_NOREFERRER = 'noopener,noreferrer';
|
||||
|
||||
const handleLinkClick = (
|
||||
e: React.MouseEvent<HTMLAnchorElement, MouseEvent>,
|
||||
): void => {
|
||||
@@ -38,13 +41,25 @@ export default function NoLogs({
|
||||
} else {
|
||||
link = ROUTES.GET_STARTED_LOGS_MANAGEMENT;
|
||||
}
|
||||
history.push(link);
|
||||
genericNavigate(link, e);
|
||||
} else if (dataSource === 'traces') {
|
||||
window.open(DOCLINKS.TRACES_EXPLORER_EMPTY_STATE, '_blank');
|
||||
window.open(
|
||||
DOCLINKS.TRACES_EXPLORER_EMPTY_STATE,
|
||||
'_blank',
|
||||
REL_NOOPENER_NOREFERRER,
|
||||
);
|
||||
} else if (dataSource === DataSource.METRICS) {
|
||||
window.open(DOCLINKS.METRICS_EXPLORER_EMPTY_STATE, '_blank');
|
||||
window.open(
|
||||
DOCLINKS.METRICS_EXPLORER_EMPTY_STATE,
|
||||
'_blank',
|
||||
REL_NOOPENER_NOREFERRER,
|
||||
);
|
||||
} else {
|
||||
window.open(`${DOCLINKS.USER_GUIDE}${dataSource}/`, '_blank');
|
||||
window.open(
|
||||
`${DOCLINKS.USER_GUIDE}${dataSource}/`,
|
||||
'_blank',
|
||||
REL_NOOPENER_NOREFERRER,
|
||||
);
|
||||
}
|
||||
};
|
||||
return (
|
||||
@@ -59,7 +74,12 @@ export default function NoLogs({
|
||||
</span>
|
||||
</Typography>
|
||||
|
||||
<Typography.Link className="send-logs-link" onClick={handleLinkClick}>
|
||||
<Typography.Link
|
||||
className="send-logs-link"
|
||||
onClick={(e: React.MouseEvent<HTMLAnchorElement, MouseEvent>): void =>
|
||||
handleLinkClick(e)
|
||||
}
|
||||
>
|
||||
Sending {dataSource} to SigNoz <ArrowUpRight size={16} />
|
||||
</Typography.Link>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
.run-query-btn {
|
||||
display: flex;
|
||||
min-width: 132px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.ant-btn-icon {
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.cancel-query-btn {
|
||||
display: flex;
|
||||
min-width: 132px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.cmd-hint {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
//not using var here to support opacity 60%. To be handled at design system level.
|
||||
background: rgba(35, 38, 46, 0.6);
|
||||
line-height: 1;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.cmd-hint {
|
||||
color: var(--bg-ink-200);
|
||||
//not using var here to support opacity 60%. To be handled at design system level.
|
||||
background: rgba(231, 232, 236, 0.8);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import './RunQueryBtn.scss';
|
||||
|
||||
import { Button } from 'antd';
|
||||
import {
|
||||
ChevronUp,
|
||||
Command,
|
||||
CornerDownLeft,
|
||||
Loader2,
|
||||
Play,
|
||||
} from 'lucide-react';
|
||||
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
|
||||
|
||||
interface RunQueryBtnProps {
|
||||
label?: string;
|
||||
isLoadingQueries?: boolean;
|
||||
handleCancelQuery?: () => void;
|
||||
onStageRunQuery?: () => void;
|
||||
}
|
||||
|
||||
function RunQueryBtn({
|
||||
label,
|
||||
isLoadingQueries,
|
||||
handleCancelQuery,
|
||||
onStageRunQuery,
|
||||
}: RunQueryBtnProps): JSX.Element {
|
||||
const isMac = getUserOperatingSystem() === UserOperatingSystem.MACOS;
|
||||
return isLoadingQueries ? (
|
||||
<Button
|
||||
type="default"
|
||||
icon={<Loader2 size={14} className="loading-icon animate-spin" />}
|
||||
className="cancel-query-btn periscope-btn danger"
|
||||
onClick={handleCancelQuery}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
className="run-query-btn periscope-btn primary"
|
||||
disabled={isLoadingQueries || !onStageRunQuery}
|
||||
onClick={onStageRunQuery}
|
||||
icon={<Play size={14} />}
|
||||
>
|
||||
{label || 'Run Query'}
|
||||
<div className="cmd-hint">
|
||||
{isMac ? <Command size={12} /> : <ChevronUp size={12} />}
|
||||
<CornerDownLeft size={12} />
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export default RunQueryBtn;
|
||||
@@ -0,0 +1,82 @@
|
||||
// frontend/src/container/QueryBuilder/components/RunQueryBtn/__tests__/RunQueryBtn.test.tsx
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
|
||||
import RunQueryBtn from '../RunQueryBtn';
|
||||
|
||||
// Mock OS util
|
||||
jest.mock('utils/getUserOS', () => ({
|
||||
getUserOperatingSystem: jest.fn(),
|
||||
UserOperatingSystem: { MACOS: 'mac', WINDOWS: 'win', LINUX: 'linux' },
|
||||
}));
|
||||
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
|
||||
|
||||
describe('RunQueryBtn', () => {
|
||||
test('renders run state and triggers on click', () => {
|
||||
(getUserOperatingSystem as jest.Mock).mockReturnValue(
|
||||
UserOperatingSystem.MACOS,
|
||||
);
|
||||
const onRun = jest.fn();
|
||||
render(<RunQueryBtn onStageRunQuery={onRun} />);
|
||||
const btn = screen.getByRole('button', { name: /run query/i });
|
||||
expect(btn).toBeEnabled();
|
||||
fireEvent.click(btn);
|
||||
expect(onRun).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('disabled when onStageRunQuery is undefined', () => {
|
||||
(getUserOperatingSystem as jest.Mock).mockReturnValue(
|
||||
UserOperatingSystem.MACOS,
|
||||
);
|
||||
render(<RunQueryBtn />);
|
||||
expect(screen.getByRole('button', { name: /run query/i })).toBeDisabled();
|
||||
});
|
||||
|
||||
test('shows cancel state and calls handleCancelQuery', () => {
|
||||
(getUserOperatingSystem as jest.Mock).mockReturnValue(
|
||||
UserOperatingSystem.MACOS,
|
||||
);
|
||||
const onCancel = jest.fn();
|
||||
render(<RunQueryBtn isLoadingQueries handleCancelQuery={onCancel} />);
|
||||
const cancel = screen.getByRole('button', { name: /cancel/i });
|
||||
fireEvent.click(cancel);
|
||||
expect(onCancel).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('shows Command + CornerDownLeft on mac', () => {
|
||||
(getUserOperatingSystem as jest.Mock).mockReturnValue(
|
||||
UserOperatingSystem.MACOS,
|
||||
);
|
||||
const { container } = render(
|
||||
<RunQueryBtn onStageRunQuery={(): void => {}} />,
|
||||
);
|
||||
expect(container.querySelector('.lucide-command')).toBeInTheDocument();
|
||||
expect(
|
||||
container.querySelector('.lucide-corner-down-left'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('shows ChevronUp + CornerDownLeft on non-mac', () => {
|
||||
(getUserOperatingSystem as jest.Mock).mockReturnValue(
|
||||
UserOperatingSystem.WINDOWS,
|
||||
);
|
||||
const { container } = render(
|
||||
<RunQueryBtn onStageRunQuery={(): void => {}} />,
|
||||
);
|
||||
expect(container.querySelector('.lucide-chevron-up')).toBeInTheDocument();
|
||||
expect(container.querySelector('.lucide-command')).not.toBeInTheDocument();
|
||||
expect(
|
||||
container.querySelector('.lucide-corner-down-left'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders custom label when provided', () => {
|
||||
(getUserOperatingSystem as jest.Mock).mockReturnValue(
|
||||
UserOperatingSystem.MACOS,
|
||||
);
|
||||
const onRun = jest.fn();
|
||||
render(<RunQueryBtn onStageRunQuery={onRun} label="Stage & Run Query" />);
|
||||
expect(
|
||||
screen.getByRole('button', { name: /stage & run query/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,12 @@
|
||||
import './ToolbarActions.styles.scss';
|
||||
|
||||
import { Button } from 'antd';
|
||||
import { LogsExplorerShortcuts } from 'constants/shortcuts/logsExplorerShortcuts';
|
||||
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||
import { Loader2, Play } from 'lucide-react';
|
||||
import { MutableRefObject, useEffect } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
|
||||
import RunQueryBtn from '../RunQueryBtn/RunQueryBtn';
|
||||
|
||||
interface RightToolbarActionsProps {
|
||||
onStageRunQuery: () => void;
|
||||
isLoadingQueries?: boolean;
|
||||
@@ -42,14 +42,7 @@ export default function RightToolbarActions({
|
||||
if (showLiveLogs) {
|
||||
return (
|
||||
<div className="right-toolbar-actions-container">
|
||||
<Button
|
||||
type="primary"
|
||||
className="run-query-btn periscope-btn primary"
|
||||
disabled
|
||||
icon={<Play size={14} />}
|
||||
>
|
||||
Run Query
|
||||
</Button>
|
||||
<RunQueryBtn />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -65,26 +58,11 @@ export default function RightToolbarActions({
|
||||
|
||||
return (
|
||||
<div className="right-toolbar-actions-container">
|
||||
{isLoadingQueries ? (
|
||||
<Button
|
||||
type="default"
|
||||
icon={<Loader2 size={14} className="loading-icon animate-spin" />}
|
||||
className="cancel-query-btn periscope-btn danger"
|
||||
onClick={handleCancelQuery}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
className="run-query-btn periscope-btn primary"
|
||||
disabled={isLoadingQueries}
|
||||
onClick={onStageRunQuery}
|
||||
icon={<Play size={14} />}
|
||||
>
|
||||
Run Query
|
||||
</Button>
|
||||
)}
|
||||
<RunQueryBtn
|
||||
isLoadingQueries={isLoadingQueries}
|
||||
handleCancelQuery={handleCancelQuery}
|
||||
onStageRunQuery={onStageRunQuery}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
|
||||
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
@@ -225,7 +226,7 @@ function QueryBuilderSearch({
|
||||
|
||||
if (
|
||||
!disableNavigationShortcuts &&
|
||||
(event.ctrlKey || event.metaKey) &&
|
||||
isCtrlOrMMetaKey(event) &&
|
||||
event.key === 'Enter'
|
||||
) {
|
||||
event.preventDefault();
|
||||
@@ -236,7 +237,7 @@ function QueryBuilderSearch({
|
||||
|
||||
if (
|
||||
!disableNavigationShortcuts &&
|
||||
(event.ctrlKey || event.metaKey) &&
|
||||
isCtrlOrMMetaKey(event) &&
|
||||
event.key === '/'
|
||||
) {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
TagFilter,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
@@ -456,12 +457,12 @@ function QueryBuilderSearchV2(
|
||||
setTags((prev) => prev.slice(0, -1));
|
||||
}
|
||||
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === '/') {
|
||||
if (isCtrlOrMMetaKey(event) && event.key === '/') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setShowAllFilters((prev) => !prev);
|
||||
}
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
|
||||
if (isCtrlOrMMetaKey(event) && event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleRunQuery();
|
||||
|
||||
@@ -136,7 +136,6 @@ const useAggregateDrilldown = ({
|
||||
query,
|
||||
// panelType,
|
||||
aggregateData: aggregateDataWithTimeRange,
|
||||
widgetId,
|
||||
onClose,
|
||||
});
|
||||
|
||||
|
||||
@@ -129,7 +129,6 @@ const useBaseAggregateOptions = ({
|
||||
|
||||
const handleBaseDrilldown = useCallback(
|
||||
(key: string): void => {
|
||||
console.log('Base drilldown:', { key, aggregateData });
|
||||
const route = getRoute(key);
|
||||
const timeRange = aggregateData?.timeRange;
|
||||
const filtersToAdd = aggregateData?.filters || [];
|
||||
|
||||
@@ -17,7 +17,6 @@ interface UseBaseAggregateOptionsProps {
|
||||
query?: Query;
|
||||
// panelType?: PANEL_TYPES;
|
||||
aggregateData?: AggregateData | null;
|
||||
widgetId?: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
@@ -27,7 +26,6 @@ const useDashboardVarConfig = ({
|
||||
query,
|
||||
// panelType,
|
||||
aggregateData,
|
||||
widgetId,
|
||||
onClose,
|
||||
}: UseBaseAggregateOptionsProps): {
|
||||
dashbaordVariablesConfig: {
|
||||
@@ -83,11 +81,6 @@ const useDashboardVarConfig = ({
|
||||
dashboardVar: [string, IDashboardVariable],
|
||||
fieldValue: any,
|
||||
) => {
|
||||
console.log('Setting variable:', {
|
||||
fieldName,
|
||||
dashboardVarId: dashboardVar[0],
|
||||
fieldValue,
|
||||
});
|
||||
onValueUpdate(fieldName, dashboardVar[1]?.id, fieldValue, false);
|
||||
onClose();
|
||||
},
|
||||
@@ -96,10 +89,6 @@ const useDashboardVarConfig = ({
|
||||
|
||||
const handleUnsetVariable = useCallback(
|
||||
(fieldName: string, dashboardVar: [string, IDashboardVariable]) => {
|
||||
console.log('Unsetting variable:', {
|
||||
fieldName,
|
||||
dashboardVarId: dashboardVar[0],
|
||||
});
|
||||
onValueUpdate(fieldName, dashboardVar[0], null, false);
|
||||
onClose();
|
||||
},
|
||||
@@ -109,12 +98,6 @@ const useDashboardVarConfig = ({
|
||||
const handleCreateVariable = useCallback(
|
||||
(fieldName: string, fieldValue: string | number | boolean) => {
|
||||
const source = getSourceFromQuery();
|
||||
console.log('Creating variable from drilldown:', {
|
||||
fieldName,
|
||||
fieldValue,
|
||||
source,
|
||||
widgetId,
|
||||
});
|
||||
createVariable(
|
||||
fieldName,
|
||||
fieldValue,
|
||||
@@ -125,7 +108,7 @@ const useDashboardVarConfig = ({
|
||||
);
|
||||
onClose();
|
||||
},
|
||||
[createVariable, getSourceFromQuery, widgetId, onClose],
|
||||
[createVariable, getSourceFromQuery, onClose],
|
||||
);
|
||||
|
||||
const contextItems = useMemo(
|
||||
|
||||
@@ -67,6 +67,8 @@ import AppReducer from 'types/reducer/app';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { checkVersionState } from 'utils/app';
|
||||
import { showErrorNotification } from 'utils/error';
|
||||
import { genericNavigate } from 'utils/genericNavigate';
|
||||
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
|
||||
|
||||
import { useCmdK } from '../../providers/cmdKProvider';
|
||||
import { routeConfig } from './config';
|
||||
@@ -292,8 +294,6 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
icon: <Cog size={16} />,
|
||||
};
|
||||
|
||||
const isCtrlMetaKey = (e: MouseEvent): boolean => e.ctrlKey || e.metaKey;
|
||||
|
||||
const isLatestVersion = checkVersionState(currentVersion, latestVersion);
|
||||
|
||||
const [
|
||||
@@ -411,7 +411,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
const isWorkspaceBlocked = trialInfo?.workSpaceBlock || false;
|
||||
|
||||
const openInNewTab = (path: string): void => {
|
||||
window.open(path, '_blank');
|
||||
window.open(path, '_blank', 'noopener,noreferrer');
|
||||
};
|
||||
|
||||
const onClickGetStarted = (event: MouseEvent): void => {
|
||||
@@ -424,7 +424,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
? ROUTES.GET_STARTED_WITH_CLOUD
|
||||
: ROUTES.GET_STARTED;
|
||||
|
||||
if (isCtrlMetaKey(event)) {
|
||||
if (isCtrlOrMMetaKey(event)) {
|
||||
openInNewTab(onboaringRoute);
|
||||
} else {
|
||||
history.push(onboaringRoute);
|
||||
@@ -439,7 +439,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
const queryString = getQueryString(availableParams || [], params);
|
||||
|
||||
if (pathname !== key) {
|
||||
if (event && isCtrlMetaKey(event)) {
|
||||
if (event && isCtrlOrMMetaKey(event)) {
|
||||
openInNewTab(`${key}?${queryString.join('&')}`);
|
||||
} else {
|
||||
history.push(`${key}?${queryString.join('&')}`, {
|
||||
@@ -634,11 +634,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
|
||||
const handleMenuItemClick = (event: MouseEvent, item: SidebarItem): void => {
|
||||
if (item.key === ROUTES.SETTINGS) {
|
||||
if (isCtrlMetaKey(event)) {
|
||||
openInNewTab(settingsRoute);
|
||||
} else {
|
||||
history.push(settingsRoute);
|
||||
}
|
||||
genericNavigate(settingsRoute, event);
|
||||
} else if (item.key === 'quick-search') {
|
||||
openCmdK();
|
||||
} else if (item) {
|
||||
@@ -809,7 +805,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
);
|
||||
|
||||
if (item && !('type' in item) && item.isExternal && item.url) {
|
||||
window.open(item.url, '_blank');
|
||||
window.open(item.url, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
|
||||
if (item && !('type' in item)) {
|
||||
|
||||
@@ -19,20 +19,6 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.cancel-query-btn {
|
||||
min-width: 96px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.run-query-btn {
|
||||
min-width: 96px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,9 +10,7 @@ import {
|
||||
import { formUrlParams } from 'container/TraceDetail/utils';
|
||||
import dayjs from 'dayjs';
|
||||
import duration from 'dayjs/plugin/duration';
|
||||
import history from 'lib/history';
|
||||
import omit from 'lodash-es/omit';
|
||||
import { HTMLAttributes } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Dispatch } from 'redux';
|
||||
import { updateURL } from 'store/actions/trace/util';
|
||||
@@ -25,6 +23,7 @@ import {
|
||||
UPDATE_SPANS_AGGREGATE_PAGE_SIZE,
|
||||
} from 'types/actions/trace';
|
||||
import { TraceReducer } from 'types/reducer/trace';
|
||||
import { genericNavigate } from 'utils/genericNavigate';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
dayjs.extend(duration);
|
||||
@@ -203,15 +202,13 @@ function TraceTable(): JSX.Element {
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onRow={(record: TableType): HTMLAttributes<TableType> => ({
|
||||
onClick: (event): void => {
|
||||
onRow={(
|
||||
record: TableType,
|
||||
): { onClick: (event: React.MouseEvent) => void } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
window.open(getLink(record), '_blank');
|
||||
} else {
|
||||
history.push(getLink(record));
|
||||
}
|
||||
genericNavigate(getLink(record), event);
|
||||
},
|
||||
})}
|
||||
pagination={{
|
||||
|
||||
@@ -84,8 +84,8 @@ function TracesTableComponent({
|
||||
onClick: (event): void => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
window.open(getTraceLink(record), '_blank');
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
window.open(getTraceLink(record), '_blank', 'noopener,noreferrer');
|
||||
} else {
|
||||
history.push(getTraceLink(record));
|
||||
}
|
||||
|
||||
@@ -31,8 +31,8 @@ export const normalizeSteps = (steps: FunnelStepData[]): FunnelStepData[] => {
|
||||
...step.filters,
|
||||
items: step.filters.items.map((item) => {
|
||||
const {
|
||||
id: unusedId,
|
||||
isIndexed,
|
||||
id: _unusedId,
|
||||
isIndexed: _isIndexed,
|
||||
...keyObj
|
||||
} = item.key as BaseAutocompleteData;
|
||||
return {
|
||||
|
||||
@@ -143,7 +143,7 @@ export const useValidateFunnelSteps = ({
|
||||
selectedTime,
|
||||
steps.map((step) => {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { latency_type, ...rest } = step;
|
||||
const { latency_type: _latency_type, ...rest } = step;
|
||||
return rest;
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -55,7 +55,7 @@ const useDragColumns = <T>(storageKey: LOCALSTORAGE): UseDragColumns<T> => {
|
||||
const parsedDraggedColumns = await JSON.parse(localStorageColumns);
|
||||
nextDraggedColumns = parsedDraggedColumns;
|
||||
} catch (e) {
|
||||
console.log('error while parsing json');
|
||||
console.error('error while parsing json: ', e);
|
||||
} finally {
|
||||
redirectWithDraggedColumns(nextDraggedColumns);
|
||||
}
|
||||
|
||||
@@ -4,11 +4,6 @@ import parser from 'lib/logql/parser';
|
||||
describe('lib/logql/parser', () => {
|
||||
test('parse valid queries', () => {
|
||||
logqlQueries.forEach((queryObject) => {
|
||||
try {
|
||||
parser(queryObject.query);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
expect(parser(queryObject.query)).toEqual(queryObject.parsedQuery);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,11 +4,7 @@ import { reverseParser } from 'lib/logql/reverseParser';
|
||||
describe('lib/logql/reverseParser', () => {
|
||||
test('reverse parse valid queries', () => {
|
||||
logqlQueries.forEach((queryObject) => {
|
||||
try {
|
||||
expect(reverseParser(queryObject.parsedQuery)).toEqual(queryObject.query);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
expect(reverseParser(queryObject.parsedQuery)).toEqual(queryObject.query);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,8 +11,8 @@ describe('getYAxisScale', () => {
|
||||
keyIndex: 1,
|
||||
thresholdValue: 10,
|
||||
thresholdUnit: 'percentunit',
|
||||
moveThreshold(dragIndex, hoverIndex): void {
|
||||
console.log(dragIndex, hoverIndex);
|
||||
moveThreshold(): void {
|
||||
// no-op
|
||||
},
|
||||
selectedGraph: PANEL_TYPES.TIME_SERIES,
|
||||
},
|
||||
@@ -21,8 +21,8 @@ describe('getYAxisScale', () => {
|
||||
keyIndex: 2,
|
||||
thresholdValue: 20,
|
||||
thresholdUnit: 'percentunit',
|
||||
moveThreshold(dragIndex, hoverIndex): void {
|
||||
console.log(dragIndex, hoverIndex);
|
||||
moveThreshold(): void {
|
||||
// no-op
|
||||
},
|
||||
selectedGraph: PANEL_TYPES.TIME_SERIES,
|
||||
},
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
MessagingQueuesPayloadProps,
|
||||
} from 'api/messagingQueues/getConsumerLagDetails';
|
||||
import axios from 'axios';
|
||||
import { isNumber } from 'chart.js/helpers';
|
||||
import cx from 'classnames';
|
||||
import { ColumnTypeRender } from 'components/Logs/TableView/types';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
|
||||
@@ -275,7 +275,7 @@ export function setConfigDetail(
|
||||
},
|
||||
): void {
|
||||
// remove "key" and its value from the paramsToSet object
|
||||
const { key, ...restParamsToSet } = paramsToSet || {};
|
||||
const { key: _key, ...restParamsToSet } = paramsToSet || {};
|
||||
|
||||
if (!isEmpty(restParamsToSet)) {
|
||||
const configDetail = {
|
||||
|
||||
@@ -145,7 +145,7 @@ export const removeFilter = (
|
||||
const updatedValues = prevValue.filter((item: any) => item !== value);
|
||||
|
||||
if (updatedValues.length === 0) {
|
||||
const { [filterType]: item, ...remainingFilters } = prevFilters;
|
||||
const { [filterType]: _item, ...remainingFilters } = prevFilters;
|
||||
return Object.keys(remainingFilters).length > 0
|
||||
? (remainingFilters as FilterType)
|
||||
: undefined;
|
||||
@@ -175,7 +175,7 @@ export const removeAllFilters = (
|
||||
return prevFilters;
|
||||
}
|
||||
|
||||
const { [filterType]: item, ...remainingFilters } = prevFilters;
|
||||
const { [filterType]: _item, ...remainingFilters } = prevFilters;
|
||||
|
||||
return Object.keys(remainingFilters).length > 0
|
||||
? (remainingFilters as Record<
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
within,
|
||||
} from 'tests/test-utils';
|
||||
import { QueryRangePayloadV5 } from 'types/api/v5/queryRange';
|
||||
import * as genericNavigate from 'utils/genericNavigate';
|
||||
|
||||
import TracesExplorer from '..';
|
||||
import { Filter } from '../Filter/Filter';
|
||||
@@ -772,6 +773,12 @@ describe('TracesExplorer - ', () => {
|
||||
});
|
||||
|
||||
it('create an alert btn assert', async () => {
|
||||
const historyPush = jest.fn();
|
||||
|
||||
jest.spyOn(genericNavigate, 'genericNavigate').mockImplementation((link) => {
|
||||
historyPush(link);
|
||||
});
|
||||
|
||||
const { getByText } = renderWithTracesExplorerRouter(<TracesExplorer />, [
|
||||
'/traces-explorer/?panelType=list&selectedExplorerView=list',
|
||||
]);
|
||||
|
||||
@@ -20,8 +20,7 @@ export const parseQueryIntoSpanKind = (
|
||||
current = parsedValue;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
console.log('error while parsing json');
|
||||
console.error('error while parsing json: ', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ export const parseQueryIntoCurrent = (
|
||||
current = parseInt(parsedValue, 10);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('error while parsing json');
|
||||
console.error('error while parsing json: ', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,7 @@ export const parseQueryIntoOrder = (
|
||||
current = parsedValue;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
console.log('error while parsing json');
|
||||
console.error('error while parsing json: ', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,7 @@ export const parseAggregateOrderParams = (
|
||||
current = parsedValue;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
console.log('error while parsing json');
|
||||
console.error('error while parsing json: ', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
18
frontend/src/utils/genericNavigate.ts
Normal file
18
frontend/src/utils/genericNavigate.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import history from 'lib/history';
|
||||
import { KeyboardEvent, MouseEvent } from 'react';
|
||||
import { isCtrlOrMMetaKey } from 'utils/isCtrlOrMMetaKey';
|
||||
|
||||
export const genericNavigate = (
|
||||
link: string,
|
||||
event?:
|
||||
| MouseEvent
|
||||
| KeyboardEvent
|
||||
| globalThis.MouseEvent
|
||||
| globalThis.KeyboardEvent,
|
||||
): void => {
|
||||
if (event && isCtrlOrMMetaKey(event)) {
|
||||
window.open(link, '_blank', 'noopener,noreferrer');
|
||||
} else {
|
||||
history.push(link);
|
||||
}
|
||||
};
|
||||
9
frontend/src/utils/isCtrlOrMMetaKey.ts
Normal file
9
frontend/src/utils/isCtrlOrMMetaKey.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { KeyboardEvent, MouseEvent } from 'react';
|
||||
|
||||
export const isCtrlOrMMetaKey = (
|
||||
event:
|
||||
| MouseEvent
|
||||
| KeyboardEvent
|
||||
| globalThis.MouseEvent
|
||||
| globalThis.KeyboardEvent,
|
||||
): boolean => event.metaKey || event.ctrlKey;
|
||||
@@ -315,5 +315,22 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/factor_password/forgot", handler.New(provider.authZ.OpenAccess(provider.userHandler.ForgotPassword), handler.OpenAPIDef{
|
||||
ID: "ForgotPassword",
|
||||
Tags: []string{"users"},
|
||||
Summary: "Forgot password",
|
||||
Description: "This endpoint initiates the forgot password flow by sending a reset password email",
|
||||
Request: new(types.PostableForgotPassword),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: []handler.OpenAPISecurityScheme{},
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
43
pkg/modules/user/config.go
Normal file
43
pkg/modules/user/config.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Password PasswordConfig `mapstructure:"password"`
|
||||
}
|
||||
type PasswordConfig struct {
|
||||
Reset ResetConfig `mapstructure:"reset"`
|
||||
}
|
||||
|
||||
type ResetConfig struct {
|
||||
AllowSelf bool `mapstructure:"allow_self"`
|
||||
MaxTokenLifetime time.Duration `mapstructure:"max_token_lifetime"`
|
||||
}
|
||||
|
||||
func NewConfigFactory() factory.ConfigFactory {
|
||||
return factory.NewConfigFactory(factory.MustNewName("user"), newConfig)
|
||||
}
|
||||
|
||||
func newConfig() factory.Config {
|
||||
return &Config{
|
||||
Password: PasswordConfig{
|
||||
Reset: ResetConfig{
|
||||
AllowSelf: false,
|
||||
MaxTokenLifetime: 6 * time.Hour,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c Config) Validate() error {
|
||||
if c.Password.Reset.MaxTokenLifetime <= 0 {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "user::password::reset::max_token_lifetime must be positive")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -332,6 +332,25 @@ func (handler *handler) ChangePassword(w http.ResponseWriter, r *http.Request) {
|
||||
render.Success(w, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
func (h *handler) ForgotPassword(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req := new(types.PostableForgotPassword)
|
||||
if err := binding.JSON.BindBody(r.Body, req); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.module.ForgotPassword(ctx, req.OrgID, req.Email, req.FrontendBaseURL)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(w, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
func (h *handler) CreateAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -12,11 +12,13 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
root "github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/tokenizer"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/emailtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/dustin/go-humanize"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
@@ -28,10 +30,11 @@ type Module struct {
|
||||
settings factory.ScopedProviderSettings
|
||||
orgSetter organization.Setter
|
||||
analytics analytics.Analytics
|
||||
config user.Config
|
||||
}
|
||||
|
||||
// This module is a WIP, don't take inspiration from this.
|
||||
func NewModule(store types.UserStore, tokenizer tokenizer.Tokenizer, emailing emailing.Emailing, providerSettings factory.ProviderSettings, orgSetter organization.Setter, analytics analytics.Analytics) root.Module {
|
||||
func NewModule(store types.UserStore, tokenizer tokenizer.Tokenizer, emailing emailing.Emailing, providerSettings factory.ProviderSettings, orgSetter organization.Setter, analytics analytics.Analytics, config user.Config) root.Module {
|
||||
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/user/impluser")
|
||||
return &Module{
|
||||
store: store,
|
||||
@@ -40,6 +43,7 @@ func NewModule(store types.UserStore, tokenizer tokenizer.Tokenizer, emailing em
|
||||
settings: settings,
|
||||
orgSetter: orgSetter,
|
||||
analytics: analytics,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,33 +306,91 @@ func (module *Module) GetOrCreateResetPasswordToken(ctx context.Context, userID
|
||||
}
|
||||
}
|
||||
|
||||
resetPasswordToken, err := types.NewResetPasswordToken(password.ID)
|
||||
// check if a token already exists for this password id
|
||||
existingResetPasswordToken, err := module.store.GetResetPasswordTokenByPasswordID(ctx, password.ID)
|
||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
return nil, err // return the error if it is not a not found error
|
||||
}
|
||||
|
||||
// return the existing token if it is not expired
|
||||
if existingResetPasswordToken != nil && !existingResetPasswordToken.IsExpired() {
|
||||
return existingResetPasswordToken, nil // return the existing token if it is not expired
|
||||
}
|
||||
|
||||
// delete the existing token entry
|
||||
if existingResetPasswordToken != nil {
|
||||
if err := module.store.DeleteResetPasswordTokenByPasswordID(ctx, password.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// create a new token
|
||||
resetPasswordToken, err := types.NewResetPasswordToken(password.ID, time.Now().Add(module.config.Password.Reset.MaxTokenLifetime))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// create a new token
|
||||
err = module.store.CreateResetPasswordToken(ctx, resetPasswordToken)
|
||||
if err != nil {
|
||||
if !errors.Ast(err, errors.TypeAlreadyExists) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if the token already exists, we return the existing token
|
||||
resetPasswordToken, err = module.store.GetResetPasswordTokenByPasswordID(ctx, password.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resetPasswordToken, nil
|
||||
}
|
||||
|
||||
func (module *Module) ForgotPassword(ctx context.Context, orgID valuer.UUID, email valuer.Email, frontendBaseURL string) error {
|
||||
if !module.config.Password.Reset.AllowSelf {
|
||||
return errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "users are not allowed to reset their password themselves, please contact an admin to reset your password")
|
||||
}
|
||||
|
||||
user, err := module.store.GetUserByEmailAndOrgID(ctx, email, orgID)
|
||||
if err != nil {
|
||||
if errors.Ast(err, errors.TypeNotFound) {
|
||||
return nil // for security reasons
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
token, err := module.GetOrCreateResetPasswordToken(ctx, user.ID)
|
||||
if err != nil {
|
||||
module.settings.Logger().ErrorContext(ctx, "failed to create reset password token", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
resetLink := fmt.Sprintf("%s/password-reset?token=%s", frontendBaseURL, token.Token)
|
||||
|
||||
tokenLifetime := module.config.Password.Reset.MaxTokenLifetime
|
||||
humanizedTokenLifetime := strings.TrimSpace(humanize.RelTime(time.Now(), time.Now().Add(tokenLifetime), "", ""))
|
||||
|
||||
if err := module.emailing.SendHTML(
|
||||
ctx,
|
||||
user.Email.String(),
|
||||
"Reset your SigNoz password",
|
||||
emailtypes.TemplateNameResetPassword,
|
||||
map[string]any{
|
||||
"Name": user.DisplayName,
|
||||
"Link": resetLink,
|
||||
"Expiry": humanizedTokenLifetime,
|
||||
},
|
||||
); err != nil {
|
||||
module.settings.Logger().ErrorContext(ctx, "failed to send reset password email", "error", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (module *Module) UpdatePasswordByResetPasswordToken(ctx context.Context, token string, passwd string) error {
|
||||
resetPasswordToken, err := module.store.GetResetPasswordToken(ctx, token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resetPasswordToken.IsExpired() {
|
||||
return errors.New(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "reset password token has expired")
|
||||
}
|
||||
|
||||
password, err := module.store.GetPassword(ctx, resetPasswordToken.PasswordID)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -391,6 +391,18 @@ func (store *store) GetResetPasswordTokenByPasswordID(ctx context.Context, passw
|
||||
return resetPasswordToken, nil
|
||||
}
|
||||
|
||||
func (store *store) DeleteResetPasswordTokenByPasswordID(ctx context.Context, passwordID valuer.UUID) error {
|
||||
_, err := store.sqlstore.BunDB().NewDelete().
|
||||
Model(&types.ResetPasswordToken{}).
|
||||
Where("password_id = ?", passwordID).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to delete reset password token")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *store) GetResetPasswordToken(ctx context.Context, token string) (*types.ResetPasswordToken, error) {
|
||||
resetPasswordRequest := new(types.ResetPasswordToken)
|
||||
|
||||
|
||||
@@ -30,6 +30,9 @@ type Module interface {
|
||||
// Updates password of user to the new password. It also deletes all reset password tokens for the user.
|
||||
UpdatePassword(ctx context.Context, userID valuer.UUID, oldPassword string, password string) error
|
||||
|
||||
// Initiate forgot password flow for a user
|
||||
ForgotPassword(ctx context.Context, orgID valuer.UUID, email valuer.Email, frontendBaseURL string) error
|
||||
|
||||
UpdateUser(ctx context.Context, orgID valuer.UUID, id string, user *types.User, updatedBy string) (*types.User, error)
|
||||
DeleteUser(ctx context.Context, orgID valuer.UUID, id string, deletedBy string) error
|
||||
|
||||
@@ -92,6 +95,7 @@ type Handler interface {
|
||||
GetResetPasswordToken(http.ResponseWriter, *http.Request)
|
||||
ResetPassword(http.ResponseWriter, *http.Request)
|
||||
ChangePassword(http.ResponseWriter, *http.Request)
|
||||
ForgotPassword(http.ResponseWriter, *http.Request)
|
||||
|
||||
// API KEY
|
||||
CreateAPIKey(http.ResponseWriter, *http.Request)
|
||||
|
||||
@@ -36,29 +36,6 @@ func NewBucketCache(settings factory.ProviderSettings, cache cache.Cache, cacheT
|
||||
}
|
||||
}
|
||||
|
||||
// cachedBucket represents a cached time bucket
|
||||
type cachedBucket struct {
|
||||
StartMs uint64 `json:"startMs"`
|
||||
EndMs uint64 `json:"endMs"`
|
||||
Type qbtypes.RequestType `json:"type"`
|
||||
Value json.RawMessage `json:"value"`
|
||||
Stats qbtypes.ExecStats `json:"stats"`
|
||||
}
|
||||
|
||||
// cachedData represents the full cached data for a query
|
||||
type cachedData struct {
|
||||
Buckets []*cachedBucket `json:"buckets"`
|
||||
Warnings []string `json:"warnings"`
|
||||
}
|
||||
|
||||
func (c *cachedData) UnmarshalBinary(data []byte) error {
|
||||
return json.Unmarshal(data, c)
|
||||
}
|
||||
|
||||
func (c *cachedData) MarshalBinary() ([]byte, error) {
|
||||
return json.Marshal(c)
|
||||
}
|
||||
|
||||
// GetMissRanges returns cached data and missing time ranges
|
||||
func (bc *bucketCache) GetMissRanges(
|
||||
ctx context.Context,
|
||||
@@ -78,7 +55,7 @@ func (bc *bucketCache) GetMissRanges(
|
||||
bc.logger.DebugContext(ctx, "cache key", "cache_key", cacheKey)
|
||||
|
||||
// Try to get cached data
|
||||
var data cachedData
|
||||
var data qbtypes.CachedData
|
||||
err := bc.cache.Get(ctx, orgID, cacheKey, &data)
|
||||
if err != nil {
|
||||
if !errors.Ast(err, errors.TypeNotFound) {
|
||||
@@ -147,9 +124,9 @@ func (bc *bucketCache) Put(ctx context.Context, orgID valuer.UUID, q qbtypes.Que
|
||||
cacheKey := bc.generateCacheKey(q)
|
||||
|
||||
// Get existing cached data
|
||||
var existingData cachedData
|
||||
var existingData qbtypes.CachedData
|
||||
if err := bc.cache.Get(ctx, orgID, cacheKey, &existingData); err != nil {
|
||||
existingData = cachedData{}
|
||||
existingData = qbtypes.CachedData{}
|
||||
}
|
||||
|
||||
// Trim the result to exclude data within flux interval
|
||||
@@ -203,7 +180,7 @@ func (bc *bucketCache) Put(ctx context.Context, orgID valuer.UUID, q qbtypes.Que
|
||||
uniqueWarnings := bc.deduplicateWarnings(allWarnings)
|
||||
|
||||
// Create updated cached data
|
||||
updatedData := cachedData{
|
||||
updatedData := qbtypes.CachedData{
|
||||
Buckets: mergedBuckets,
|
||||
Warnings: uniqueWarnings,
|
||||
}
|
||||
@@ -222,7 +199,7 @@ func (bc *bucketCache) generateCacheKey(q qbtypes.Query) string {
|
||||
}
|
||||
|
||||
// findMissingRangesWithStep identifies time ranges not covered by cached buckets with step alignment
|
||||
func (bc *bucketCache) findMissingRangesWithStep(buckets []*cachedBucket, startMs, endMs uint64, stepMs uint64) []*qbtypes.TimeRange {
|
||||
func (bc *bucketCache) findMissingRangesWithStep(buckets []*qbtypes.CachedBucket, startMs, endMs uint64, stepMs uint64) []*qbtypes.TimeRange {
|
||||
// When step is 0 or window is too small to be cached, use simple algorithm
|
||||
if stepMs == 0 || (startMs+stepMs) > endMs {
|
||||
return bc.findMissingRangesBasic(buckets, startMs, endMs)
|
||||
@@ -265,7 +242,7 @@ func (bc *bucketCache) findMissingRangesWithStep(buckets []*cachedBucket, startM
|
||||
}
|
||||
|
||||
if needsSort {
|
||||
slices.SortFunc(buckets, func(a, b *cachedBucket) int {
|
||||
slices.SortFunc(buckets, func(a, b *qbtypes.CachedBucket) int {
|
||||
if a.StartMs < b.StartMs {
|
||||
return -1
|
||||
}
|
||||
@@ -339,7 +316,7 @@ func (bc *bucketCache) findMissingRangesWithStep(buckets []*cachedBucket, startM
|
||||
}
|
||||
|
||||
// findMissingRangesBasic is the simple algorithm without step alignment
|
||||
func (bc *bucketCache) findMissingRangesBasic(buckets []*cachedBucket, startMs, endMs uint64) []*qbtypes.TimeRange {
|
||||
func (bc *bucketCache) findMissingRangesBasic(buckets []*qbtypes.CachedBucket, startMs, endMs uint64) []*qbtypes.TimeRange {
|
||||
// Check if already sorted before sorting
|
||||
needsSort := false
|
||||
for i := 1; i < len(buckets); i++ {
|
||||
@@ -350,7 +327,7 @@ func (bc *bucketCache) findMissingRangesBasic(buckets []*cachedBucket, startMs,
|
||||
}
|
||||
|
||||
if needsSort {
|
||||
slices.SortFunc(buckets, func(a, b *cachedBucket) int {
|
||||
slices.SortFunc(buckets, func(a, b *qbtypes.CachedBucket) int {
|
||||
if a.StartMs < b.StartMs {
|
||||
return -1
|
||||
}
|
||||
@@ -421,9 +398,9 @@ func (bc *bucketCache) findMissingRangesBasic(buckets []*cachedBucket, startMs,
|
||||
}
|
||||
|
||||
// filterRelevantBuckets returns buckets that overlap with the requested time range
|
||||
func (bc *bucketCache) filterRelevantBuckets(buckets []*cachedBucket, startMs, endMs uint64) []*cachedBucket {
|
||||
func (bc *bucketCache) filterRelevantBuckets(buckets []*qbtypes.CachedBucket, startMs, endMs uint64) []*qbtypes.CachedBucket {
|
||||
// Pre-allocate with estimated capacity
|
||||
relevant := make([]*cachedBucket, 0, len(buckets))
|
||||
relevant := make([]*qbtypes.CachedBucket, 0, len(buckets))
|
||||
|
||||
for _, bucket := range buckets {
|
||||
// Check if bucket overlaps with requested range
|
||||
@@ -433,7 +410,7 @@ func (bc *bucketCache) filterRelevantBuckets(buckets []*cachedBucket, startMs, e
|
||||
}
|
||||
|
||||
// Sort by start time
|
||||
slices.SortFunc(relevant, func(a, b *cachedBucket) int {
|
||||
slices.SortFunc(relevant, func(a, b *qbtypes.CachedBucket) int {
|
||||
if a.StartMs < b.StartMs {
|
||||
return -1
|
||||
}
|
||||
@@ -447,7 +424,7 @@ func (bc *bucketCache) filterRelevantBuckets(buckets []*cachedBucket, startMs, e
|
||||
}
|
||||
|
||||
// mergeBuckets combines multiple cached buckets into a single result
|
||||
func (bc *bucketCache) mergeBuckets(ctx context.Context, buckets []*cachedBucket, warnings []string) *qbtypes.Result {
|
||||
func (bc *bucketCache) mergeBuckets(ctx context.Context, buckets []*qbtypes.CachedBucket, warnings []string) *qbtypes.Result {
|
||||
if len(buckets) == 0 {
|
||||
return &qbtypes.Result{}
|
||||
}
|
||||
@@ -480,7 +457,7 @@ func (bc *bucketCache) mergeBuckets(ctx context.Context, buckets []*cachedBucket
|
||||
}
|
||||
|
||||
// mergeTimeSeriesValues merges time series data from multiple buckets
|
||||
func (bc *bucketCache) mergeTimeSeriesValues(ctx context.Context, buckets []*cachedBucket) *qbtypes.TimeSeriesData {
|
||||
func (bc *bucketCache) mergeTimeSeriesValues(ctx context.Context, buckets []*qbtypes.CachedBucket) *qbtypes.TimeSeriesData {
|
||||
// Estimate capacity based on bucket count
|
||||
estimatedSeries := len(buckets) * 10
|
||||
|
||||
@@ -631,7 +608,7 @@ func (bc *bucketCache) isEmptyResult(result *qbtypes.Result) (isEmpty bool, isFi
|
||||
}
|
||||
|
||||
// resultToBuckets converts a query result into time-based buckets
|
||||
func (bc *bucketCache) resultToBuckets(ctx context.Context, result *qbtypes.Result, startMs, endMs uint64) []*cachedBucket {
|
||||
func (bc *bucketCache) resultToBuckets(ctx context.Context, result *qbtypes.Result, startMs, endMs uint64) []*qbtypes.CachedBucket {
|
||||
// Check if result is empty
|
||||
isEmpty, isFiltered := bc.isEmptyResult(result)
|
||||
|
||||
@@ -652,7 +629,7 @@ func (bc *bucketCache) resultToBuckets(ctx context.Context, result *qbtypes.Resu
|
||||
|
||||
// Always create a bucket, even for empty filtered results
|
||||
// This ensures we don't re-query for data that doesn't exist
|
||||
return []*cachedBucket{
|
||||
return []*qbtypes.CachedBucket{
|
||||
{
|
||||
StartMs: startMs,
|
||||
EndMs: endMs,
|
||||
@@ -664,9 +641,9 @@ func (bc *bucketCache) resultToBuckets(ctx context.Context, result *qbtypes.Resu
|
||||
}
|
||||
|
||||
// mergeAndDeduplicateBuckets combines and deduplicates bucket lists
|
||||
func (bc *bucketCache) mergeAndDeduplicateBuckets(existing, fresh []*cachedBucket) []*cachedBucket {
|
||||
func (bc *bucketCache) mergeAndDeduplicateBuckets(existing, fresh []*qbtypes.CachedBucket) []*qbtypes.CachedBucket {
|
||||
// Create a map to deduplicate by time range
|
||||
bucketMap := make(map[string]*cachedBucket)
|
||||
bucketMap := make(map[string]*qbtypes.CachedBucket)
|
||||
|
||||
// Add existing buckets
|
||||
for _, bucket := range existing {
|
||||
@@ -681,13 +658,13 @@ func (bc *bucketCache) mergeAndDeduplicateBuckets(existing, fresh []*cachedBucke
|
||||
}
|
||||
|
||||
// Convert back to slice with pre-allocated capacity
|
||||
result := make([]*cachedBucket, 0, len(bucketMap))
|
||||
result := make([]*qbtypes.CachedBucket, 0, len(bucketMap))
|
||||
for _, bucket := range bucketMap {
|
||||
result = append(result, bucket)
|
||||
}
|
||||
|
||||
// Sort by start time
|
||||
slices.SortFunc(result, func(a, b *cachedBucket) int {
|
||||
slices.SortFunc(result, func(a, b *qbtypes.CachedBucket) int {
|
||||
if a.StartMs < b.StartMs {
|
||||
return -1
|
||||
}
|
||||
|
||||
@@ -147,14 +147,14 @@ func BenchmarkBucketCache_MergeTimeSeriesValues(b *testing.B) {
|
||||
for _, tc := range testCases {
|
||||
b.Run(tc.name, func(b *testing.B) {
|
||||
// Create test buckets
|
||||
buckets := make([]*cachedBucket, tc.numBuckets)
|
||||
buckets := make([]*qbtypes.CachedBucket, tc.numBuckets)
|
||||
for i := 0; i < tc.numBuckets; i++ {
|
||||
startMs := uint64(i * 10000)
|
||||
endMs := uint64((i + 1) * 10000)
|
||||
result := createBenchmarkResultWithSeries(startMs, endMs, 1000, tc.numSeries, tc.numValues)
|
||||
|
||||
valueBytes, _ := json.Marshal(result.Value)
|
||||
buckets[i] = &cachedBucket{
|
||||
buckets[i] = &qbtypes.CachedBucket{
|
||||
StartMs: startMs,
|
||||
EndMs: endMs,
|
||||
Type: qbtypes.RequestTypeTimeSeries,
|
||||
@@ -417,8 +417,8 @@ func createBenchmarkResultWithSeries(startMs, endMs uint64, _ uint64, numSeries,
|
||||
}
|
||||
|
||||
// Helper function to create buckets with specific gap patterns
|
||||
func createBucketsWithPattern(numBuckets int, pattern string) []*cachedBucket {
|
||||
buckets := make([]*cachedBucket, 0, numBuckets)
|
||||
func createBucketsWithPattern(numBuckets int, pattern string) []*qbtypes.CachedBucket {
|
||||
buckets := make([]*qbtypes.CachedBucket, 0, numBuckets)
|
||||
|
||||
for i := 0; i < numBuckets; i++ {
|
||||
// Skip some buckets based on pattern
|
||||
@@ -432,7 +432,7 @@ func createBucketsWithPattern(numBuckets int, pattern string) []*cachedBucket {
|
||||
startMs := uint64(i * 10000)
|
||||
endMs := uint64((i + 1) * 10000)
|
||||
|
||||
buckets = append(buckets, &cachedBucket{
|
||||
buckets = append(buckets, &qbtypes.CachedBucket{
|
||||
StartMs: startMs,
|
||||
EndMs: endMs,
|
||||
Type: qbtypes.RequestTypeTimeSeries,
|
||||
|
||||
@@ -521,7 +521,7 @@ func TestBucketCache_FindMissingRanges_EdgeCases(t *testing.T) {
|
||||
bc := NewBucketCache(instrumentationtest.New().ToProviderSettings(), memCache, cacheTTL, defaultFluxInterval).(*bucketCache)
|
||||
|
||||
// Test with buckets that have gaps and overlaps
|
||||
buckets := []*cachedBucket{
|
||||
buckets := []*qbtypes.CachedBucket{
|
||||
{StartMs: 1000, EndMs: 2000},
|
||||
{StartMs: 2500, EndMs: 3500},
|
||||
{StartMs: 3000, EndMs: 4000}, // Overlaps with previous
|
||||
@@ -1097,7 +1097,7 @@ func TestBucketCache_FindMissingRangesWithStep(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
buckets []*cachedBucket
|
||||
buckets []*qbtypes.CachedBucket
|
||||
startMs uint64
|
||||
endMs uint64
|
||||
stepMs uint64
|
||||
@@ -1106,7 +1106,7 @@ func TestBucketCache_FindMissingRangesWithStep(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "start_not_aligned_to_step",
|
||||
buckets: []*cachedBucket{},
|
||||
buckets: []*qbtypes.CachedBucket{},
|
||||
startMs: 1500, // Not aligned to 1000ms step
|
||||
endMs: 5000,
|
||||
stepMs: 1000,
|
||||
@@ -1118,7 +1118,7 @@ func TestBucketCache_FindMissingRangesWithStep(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "end_not_aligned_to_step",
|
||||
buckets: []*cachedBucket{},
|
||||
buckets: []*qbtypes.CachedBucket{},
|
||||
startMs: 1000,
|
||||
endMs: 4500, // Not aligned to 1000ms step
|
||||
stepMs: 1000,
|
||||
@@ -1129,7 +1129,7 @@ func TestBucketCache_FindMissingRangesWithStep(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "bucket_boundaries_not_aligned",
|
||||
buckets: []*cachedBucket{
|
||||
buckets: []*qbtypes.CachedBucket{
|
||||
{StartMs: 1500, EndMs: 2500}, // Not aligned
|
||||
},
|
||||
startMs: 1000,
|
||||
@@ -1143,7 +1143,7 @@ func TestBucketCache_FindMissingRangesWithStep(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "small_window_less_than_step",
|
||||
buckets: []*cachedBucket{},
|
||||
buckets: []*qbtypes.CachedBucket{},
|
||||
startMs: 1000,
|
||||
endMs: 1500, // Less than one step
|
||||
stepMs: 1000,
|
||||
@@ -1154,7 +1154,7 @@ func TestBucketCache_FindMissingRangesWithStep(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "zero_step_uses_basic_algorithm",
|
||||
buckets: []*cachedBucket{},
|
||||
buckets: []*qbtypes.CachedBucket{},
|
||||
startMs: 1000,
|
||||
endMs: 5000,
|
||||
stepMs: 0,
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/global"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/ruler"
|
||||
@@ -109,6 +110,9 @@ type Config struct {
|
||||
|
||||
// Flagger config
|
||||
Flagger flagger.Config `mapstructure:"flagger"`
|
||||
|
||||
// User config
|
||||
User user.Config `mapstructure:"user"`
|
||||
}
|
||||
|
||||
// DeprecatedFlags are the flags that are deprecated and scheduled for removal.
|
||||
@@ -171,6 +175,7 @@ func NewConfig(ctx context.Context, logger *slog.Logger, resolverConfig config.R
|
||||
tokenizer.NewConfigFactory(),
|
||||
metricsexplorer.NewConfigFactory(),
|
||||
flagger.NewConfigFactory(),
|
||||
user.NewConfigFactory(),
|
||||
}
|
||||
|
||||
conf, err := config.New(ctx, resolverConfig, configFactories)
|
||||
|
||||
@@ -88,7 +88,7 @@ func NewModules(
|
||||
) Modules {
|
||||
quickfilter := implquickfilter.NewModule(implquickfilter.NewStore(sqlstore))
|
||||
orgSetter := implorganization.NewSetter(implorganization.NewStore(sqlstore), alertmanager, quickfilter)
|
||||
user := impluser.NewModule(impluser.NewStore(sqlstore, providerSettings), tokenizer, emailing, providerSettings, orgSetter, analytics)
|
||||
user := impluser.NewModule(impluser.NewStore(sqlstore, providerSettings), tokenizer, emailing, providerSettings, orgSetter, analytics, config.User)
|
||||
userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings))
|
||||
ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings)
|
||||
|
||||
|
||||
@@ -161,6 +161,7 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewUpdateUserPreferenceFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewUpdateOrgPreferenceFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewRenameOrgDomainsFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewAddResetPasswordTokenExpiryFactory(sqlstore, sqlschema),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
83
pkg/sqlmigration/058_add_reset_password_token_expiry.go
Normal file
83
pkg/sqlmigration/058_add_reset_password_token_expiry.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
type addResetPasswordTokenExpiry struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
sqlschema sqlschema.SQLSchema
|
||||
}
|
||||
|
||||
func NewAddResetPasswordTokenExpiryFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("add_reset_password_token_expiry"), func(ctx context.Context, providerSettings factory.ProviderSettings, config Config) (SQLMigration, error) {
|
||||
return newAddResetPasswordTokenExpiry(ctx, providerSettings, config, sqlstore, sqlschema)
|
||||
})
|
||||
}
|
||||
|
||||
func newAddResetPasswordTokenExpiry(_ context.Context, _ factory.ProviderSettings, _ Config, sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) (SQLMigration, error) {
|
||||
return &addResetPasswordTokenExpiry{
|
||||
sqlstore: sqlstore,
|
||||
sqlschema: sqlschema,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (migration *addResetPasswordTokenExpiry) Register(migrations *migrate.Migrations) error {
|
||||
if err := migrations.Register(migration.Up, migration.Down); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *addResetPasswordTokenExpiry) Up(ctx context.Context, db *bun.DB) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
// get the reset_password_token table
|
||||
table, uniqueConstraints, err := migration.sqlschema.GetTable(ctx, sqlschema.TableName("reset_password_token"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add a new column `expires_at`
|
||||
column := &sqlschema.Column{
|
||||
Name: sqlschema.ColumnName("expires_at"),
|
||||
DataType: sqlschema.DataTypeTimestamp,
|
||||
Nullable: true,
|
||||
}
|
||||
|
||||
// for existing rows set
|
||||
defaultValueForExistingRows := time.Now()
|
||||
|
||||
sqls := migration.sqlschema.Operator().AddColumn(table, uniqueConstraints, column, defaultValueForExistingRows)
|
||||
|
||||
for _, sql := range sqls {
|
||||
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *addResetPasswordTokenExpiry) Down(ctx context.Context, db *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
@@ -279,7 +279,7 @@ func (c *conditionBuilder) buildSpanScopeCondition(key *telemetrytypes.Telemetry
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "span scope field %s only supports '=' operator", key.Name)
|
||||
}
|
||||
|
||||
isTrue := false
|
||||
var isTrue bool
|
||||
switch v := value.(type) {
|
||||
case bool:
|
||||
isTrue = v
|
||||
|
||||
@@ -12,12 +12,13 @@ import (
|
||||
var (
|
||||
// Templates is a list of all the templates that are supported by the emailing service.
|
||||
// This list should be updated whenever a new template is added.
|
||||
Templates = []TemplateName{TemplateNameInvitationEmail, TemplateNameUpdateRole}
|
||||
Templates = []TemplateName{TemplateNameInvitationEmail, TemplateNameUpdateRole, TemplateNameResetPassword}
|
||||
)
|
||||
|
||||
var (
|
||||
TemplateNameInvitationEmail = TemplateName{valuer.NewString("invitation_email")}
|
||||
TemplateNameUpdateRole = TemplateName{valuer.NewString("update_role")}
|
||||
TemplateNameResetPassword = TemplateName{valuer.NewString("reset_password_email")}
|
||||
)
|
||||
|
||||
type TemplateName struct{ valuer.String }
|
||||
@@ -28,6 +29,8 @@ func NewTemplateName(name string) (TemplateName, error) {
|
||||
return TemplateNameInvitationEmail, nil
|
||||
case TemplateNameUpdateRole.StringValue():
|
||||
return TemplateNameUpdateRole, nil
|
||||
case TemplateNameResetPassword.StringValue():
|
||||
return TemplateNameResetPassword, nil
|
||||
default:
|
||||
return TemplateName{}, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid template name: %s", name)
|
||||
}
|
||||
|
||||
@@ -35,12 +35,19 @@ type ChangePasswordRequest struct {
|
||||
NewPassword string `json:"newPassword"`
|
||||
}
|
||||
|
||||
type PostableForgotPassword struct {
|
||||
OrgID valuer.UUID `json:"orgId"`
|
||||
Email valuer.Email `json:"email"`
|
||||
FrontendBaseURL string `json:"frontendBaseURL"`
|
||||
}
|
||||
|
||||
type ResetPasswordToken struct {
|
||||
bun.BaseModel `bun:"table:reset_password_token"`
|
||||
|
||||
Identifiable
|
||||
Token string `bun:"token,type:text,notnull" json:"token"`
|
||||
PasswordID valuer.UUID `bun:"password_id,type:text,notnull,unique" json:"passwordId"`
|
||||
ExpiresAt time.Time `bun:"expires_at,type:timestamptz,nullzero" json:"expiresAt"`
|
||||
}
|
||||
|
||||
type FactorPassword struct {
|
||||
@@ -136,13 +143,14 @@ func NewHashedPassword(password string) (string, error) {
|
||||
return string(hashedPassword), nil
|
||||
}
|
||||
|
||||
func NewResetPasswordToken(passwordID valuer.UUID) (*ResetPasswordToken, error) {
|
||||
func NewResetPasswordToken(passwordID valuer.UUID, expiresAt time.Time) (*ResetPasswordToken, error) {
|
||||
return &ResetPasswordToken{
|
||||
Identifiable: Identifiable{
|
||||
ID: valuer.GenerateUUID(),
|
||||
},
|
||||
Token: valuer.GenerateUUID().String(),
|
||||
PasswordID: passwordID,
|
||||
ExpiresAt: expiresAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -208,3 +216,7 @@ func (f *FactorPassword) Equals(password string) bool {
|
||||
func comparePassword(hashedPassword string, password string) bool {
|
||||
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) == nil
|
||||
}
|
||||
|
||||
func (r *ResetPasswordToken) IsExpired() bool {
|
||||
return r.ExpiresAt.Before(time.Now())
|
||||
}
|
||||
|
||||
@@ -553,6 +553,18 @@ func (f Function) Copy() Function {
|
||||
return c
|
||||
}
|
||||
|
||||
// Validate validates the name and args for the function
|
||||
func (f Function) Validate() error {
|
||||
if err := f.Name.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
// Validate args for function
|
||||
if err := f.ValidateArgs(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type LimitBy struct {
|
||||
// keys to limit by
|
||||
Keys []string `json:"keys"`
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package querybuildertypesv5
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
@@ -131,12 +133,44 @@ func (q *QueryBuilderQuery[T]) UnmarshalJSON(data []byte) error {
|
||||
|
||||
var temp Alias
|
||||
// Use UnmarshalJSONWithContext for better error messages
|
||||
if err := UnmarshalJSONWithContext(data, &temp, "query spec"); err != nil {
|
||||
if err := UnmarshalJSONWithContext(data, &temp, fmt.Sprintf("query spec for %T", q)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Copy the decoded values back to the original struct
|
||||
*q = QueryBuilderQuery[T](temp)
|
||||
|
||||
// Nomarlize the query after unmarshaling
|
||||
q.Normalize()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Normalize normalizes all the field keys in the query
|
||||
func (q *QueryBuilderQuery[T]) Normalize() {
|
||||
|
||||
// normalize select fields
|
||||
for idx := range q.SelectFields {
|
||||
q.SelectFields[idx].Normalize()
|
||||
}
|
||||
|
||||
// normalize group by fields
|
||||
for idx := range q.GroupBy {
|
||||
q.GroupBy[idx].Normalize()
|
||||
}
|
||||
|
||||
// normalize order by fields
|
||||
for idx := range q.Order {
|
||||
q.Order[idx].Key.Normalize()
|
||||
}
|
||||
|
||||
// normalize secondary aggregations
|
||||
for idx := range q.SecondaryAggregations {
|
||||
for jdx := range q.SecondaryAggregations[idx].Order {
|
||||
q.SecondaryAggregations[idx].Order[jdx].Key.Normalize()
|
||||
}
|
||||
for jdx := range q.SecondaryAggregations[idx].GroupBy {
|
||||
q.SecondaryAggregations[idx].GroupBy[jdx].Normalize()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,653 @@
|
||||
package querybuildertypesv5
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/metrictypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestQueryBuilderQuery_Copy(t *testing.T) {
|
||||
t.Run("copy with all fields populated", func(t *testing.T) {
|
||||
original := QueryBuilderQuery[TraceAggregation]{
|
||||
Name: "A",
|
||||
StepInterval: Step{Duration: 60 * time.Second},
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
Source: telemetrytypes.SourceUnspecified,
|
||||
Aggregations: []TraceAggregation{
|
||||
{
|
||||
Expression: "count()",
|
||||
Alias: "trace_count",
|
||||
},
|
||||
},
|
||||
Disabled: false,
|
||||
Filter: &Filter{
|
||||
Expression: "service.name = 'frontend'",
|
||||
},
|
||||
GroupBy: []GroupByKey{
|
||||
{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: "service.name",
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
},
|
||||
},
|
||||
},
|
||||
Order: []OrderBy{
|
||||
{
|
||||
Key: OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: "timestamp",
|
||||
FieldContext: telemetrytypes.FieldContextSpan,
|
||||
},
|
||||
},
|
||||
Direction: OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
SelectFields: []telemetrytypes.TelemetryFieldKey{
|
||||
{
|
||||
Name: "trace_id",
|
||||
FieldContext: telemetrytypes.FieldContextSpan,
|
||||
},
|
||||
},
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
Cursor: "cursor123",
|
||||
LimitBy: &LimitBy{
|
||||
Value: "10",
|
||||
Keys: []string{
|
||||
"service.name",
|
||||
},
|
||||
},
|
||||
Having: &Having{
|
||||
Expression: "count() > 100",
|
||||
},
|
||||
SecondaryAggregations: []SecondaryAggregation{
|
||||
{
|
||||
Limit: 10,
|
||||
Order: []OrderBy{
|
||||
{
|
||||
Key: OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: "value",
|
||||
FieldContext: telemetrytypes.FieldContextSpan,
|
||||
},
|
||||
},
|
||||
Direction: OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
GroupBy: []GroupByKey{
|
||||
{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: "region",
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Functions: []Function{
|
||||
{
|
||||
Name: FunctionNameTimeShift,
|
||||
Args: []FunctionArg{
|
||||
{
|
||||
Name: "shift",
|
||||
Value: "1h",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Legend: "{{service.name}}",
|
||||
ShiftBy: 3600000,
|
||||
}
|
||||
|
||||
// Create a copy
|
||||
copied := original.Copy()
|
||||
|
||||
// Assert that values are equal
|
||||
assert.Equal(t, original.Name, copied.Name)
|
||||
assert.Equal(t, original.StepInterval, copied.StepInterval)
|
||||
assert.Equal(t, original.Signal, copied.Signal)
|
||||
assert.Equal(t, original.Source, copied.Source)
|
||||
assert.Equal(t, original.Disabled, copied.Disabled)
|
||||
assert.Equal(t, original.Limit, copied.Limit)
|
||||
assert.Equal(t, original.Offset, copied.Offset)
|
||||
assert.Equal(t, original.Cursor, copied.Cursor)
|
||||
assert.Equal(t, original.Legend, copied.Legend)
|
||||
assert.Equal(t, original.ShiftBy, copied.ShiftBy)
|
||||
|
||||
// Assert deep copies for slices and pointers
|
||||
require.NotNil(t, copied.Aggregations)
|
||||
assert.Equal(t, len(original.Aggregations), len(copied.Aggregations))
|
||||
assert.Equal(t, original.Aggregations[0].Expression, copied.Aggregations[0].Expression)
|
||||
|
||||
require.NotNil(t, copied.Filter)
|
||||
assert.Equal(t, original.Filter.Expression, copied.Filter.Expression)
|
||||
|
||||
require.NotNil(t, copied.GroupBy)
|
||||
assert.Equal(t, len(original.GroupBy), len(copied.GroupBy))
|
||||
assert.Equal(t, original.GroupBy[0].Name, copied.GroupBy[0].Name)
|
||||
|
||||
require.NotNil(t, copied.Order)
|
||||
assert.Equal(t, len(original.Order), len(copied.Order))
|
||||
assert.Equal(t, original.Order[0].Key.Name, copied.Order[0].Key.Name)
|
||||
|
||||
require.NotNil(t, copied.SelectFields)
|
||||
assert.Equal(t, len(original.SelectFields), len(copied.SelectFields))
|
||||
assert.Equal(t, original.SelectFields[0].Name, copied.SelectFields[0].Name)
|
||||
|
||||
require.NotNil(t, copied.LimitBy)
|
||||
assert.Equal(t, original.LimitBy.Value, copied.LimitBy.Value)
|
||||
assert.Equal(t, len(original.LimitBy.Keys), len(copied.LimitBy.Keys))
|
||||
|
||||
require.NotNil(t, copied.Having)
|
||||
assert.Equal(t, original.Having.Expression, copied.Having.Expression)
|
||||
|
||||
require.NotNil(t, copied.SecondaryAggregations)
|
||||
assert.Equal(t, len(original.SecondaryAggregations), len(copied.SecondaryAggregations))
|
||||
assert.Equal(t, original.SecondaryAggregations[0].Limit, copied.SecondaryAggregations[0].Limit)
|
||||
|
||||
require.NotNil(t, copied.Functions)
|
||||
assert.Equal(t, len(original.Functions), len(copied.Functions))
|
||||
assert.Equal(t, original.Functions[0].Name, copied.Functions[0].Name)
|
||||
|
||||
// Verify independence - modify copied and ensure original is unchanged
|
||||
copied.Name = "B"
|
||||
assert.Equal(t, "A", original.Name)
|
||||
|
||||
copied.Aggregations[0].Expression = "sum()"
|
||||
assert.Equal(t, "count()", original.Aggregations[0].Expression)
|
||||
|
||||
copied.Filter.Expression = "modified"
|
||||
assert.Equal(t, "service.name = 'frontend'", original.Filter.Expression)
|
||||
|
||||
copied.GroupBy[0].Name = "modified"
|
||||
assert.Equal(t, "service.name", original.GroupBy[0].Name)
|
||||
|
||||
copied.Order[0].Key.Name = "modified"
|
||||
assert.Equal(t, "timestamp", original.Order[0].Key.Name)
|
||||
|
||||
copied.SelectFields[0].Name = "modified"
|
||||
assert.Equal(t, "trace_id", original.SelectFields[0].Name)
|
||||
|
||||
copied.LimitBy.Value = "999"
|
||||
assert.Equal(t, "10", original.LimitBy.Value)
|
||||
|
||||
copied.Having.Expression = "modified"
|
||||
assert.Equal(t, "count() > 100", original.Having.Expression)
|
||||
|
||||
copied.SecondaryAggregations[0].Limit = 999
|
||||
assert.Equal(t, 10, original.SecondaryAggregations[0].Limit)
|
||||
|
||||
copied.Functions[0].Name = FunctionNameAbsolute
|
||||
assert.Equal(t, FunctionNameTimeShift, original.Functions[0].Name)
|
||||
})
|
||||
|
||||
t.Run("copy with nil fields", func(t *testing.T) {
|
||||
original := QueryBuilderQuery[TraceAggregation]{
|
||||
Name: "A",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
}
|
||||
|
||||
copied := original.Copy()
|
||||
|
||||
assert.Equal(t, original.Name, copied.Name)
|
||||
assert.Equal(t, original.Signal, copied.Signal)
|
||||
assert.Nil(t, copied.Aggregations)
|
||||
assert.Nil(t, copied.Filter)
|
||||
assert.Nil(t, copied.GroupBy)
|
||||
assert.Nil(t, copied.Order)
|
||||
assert.Nil(t, copied.SelectFields)
|
||||
assert.Nil(t, copied.LimitBy)
|
||||
assert.Nil(t, copied.Having)
|
||||
assert.Nil(t, copied.SecondaryAggregations)
|
||||
assert.Nil(t, copied.Functions)
|
||||
})
|
||||
|
||||
t.Run("copy metric aggregation query", func(t *testing.T) {
|
||||
original := QueryBuilderQuery[MetricAggregation]{
|
||||
Name: "M",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []MetricAggregation{
|
||||
{
|
||||
MetricName: "cpu_usage",
|
||||
SpaceAggregation: metrictypes.SpaceAggregationAvg,
|
||||
TimeAggregation: metrictypes.TimeAggregationAvg,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
copied := original.Copy()
|
||||
|
||||
assert.Equal(t, original.Name, copied.Name)
|
||||
assert.Equal(t, original.Signal, copied.Signal)
|
||||
require.NotNil(t, copied.Aggregations)
|
||||
assert.Equal(t, original.Aggregations[0].MetricName, copied.Aggregations[0].MetricName)
|
||||
assert.Equal(t, original.Aggregations[0].SpaceAggregation, copied.Aggregations[0].SpaceAggregation)
|
||||
|
||||
// Verify independence
|
||||
copied.Aggregations[0].MetricName = "modified"
|
||||
assert.Equal(t, "cpu_usage", original.Aggregations[0].MetricName)
|
||||
})
|
||||
|
||||
t.Run("copy log aggregation query", func(t *testing.T) {
|
||||
original := QueryBuilderQuery[LogAggregation]{
|
||||
Name: "L",
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Aggregations: []LogAggregation{
|
||||
{
|
||||
Expression: "count()",
|
||||
Alias: "log_count",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
copied := original.Copy()
|
||||
|
||||
assert.Equal(t, original.Name, copied.Name)
|
||||
assert.Equal(t, original.Signal, copied.Signal)
|
||||
require.NotNil(t, copied.Aggregations)
|
||||
assert.Equal(t, original.Aggregations[0].Expression, copied.Aggregations[0].Expression)
|
||||
|
||||
// Verify independence
|
||||
copied.Aggregations[0].Expression = "sum()"
|
||||
assert.Equal(t, "count()", original.Aggregations[0].Expression)
|
||||
})
|
||||
}
|
||||
|
||||
func TestQueryBuilderQuery_Normalize(t *testing.T) {
|
||||
t.Run("normalize select fields", func(t *testing.T) {
|
||||
query := QueryBuilderQuery[TraceAggregation]{
|
||||
SelectFields: []telemetrytypes.TelemetryFieldKey{
|
||||
{
|
||||
Name: "service.name",
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
},
|
||||
{
|
||||
Name: "span.name",
|
||||
FieldContext: telemetrytypes.FieldContextSpan,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
query.Normalize()
|
||||
|
||||
// Normalize only changes FieldContext, not the Name
|
||||
assert.Equal(t, "service.name", query.SelectFields[0].Name)
|
||||
assert.Equal(t, telemetrytypes.FieldContextResource, query.SelectFields[0].FieldContext)
|
||||
assert.Equal(t, "span.name", query.SelectFields[1].Name)
|
||||
assert.Equal(t, telemetrytypes.FieldContextSpan, query.SelectFields[1].FieldContext)
|
||||
})
|
||||
|
||||
t.Run("normalize group by fields", func(t *testing.T) {
|
||||
query := QueryBuilderQuery[TraceAggregation]{
|
||||
GroupBy: []GroupByKey{
|
||||
{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: "service.name",
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
query.Normalize()
|
||||
|
||||
assert.Equal(t, "service.name", query.GroupBy[0].Name)
|
||||
assert.Equal(t, telemetrytypes.FieldContextResource, query.GroupBy[0].FieldContext)
|
||||
})
|
||||
|
||||
t.Run("normalize order by fields", func(t *testing.T) {
|
||||
query := QueryBuilderQuery[TraceAggregation]{
|
||||
Order: []OrderBy{
|
||||
{
|
||||
Key: OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: "timestamp",
|
||||
FieldContext: telemetrytypes.FieldContextSpan,
|
||||
},
|
||||
},
|
||||
Direction: OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
query.Normalize()
|
||||
|
||||
assert.Equal(t, "timestamp", query.Order[0].Key.Name)
|
||||
assert.Equal(t, telemetrytypes.FieldContextSpan, query.Order[0].Key.FieldContext)
|
||||
})
|
||||
|
||||
t.Run("normalize secondary aggregations", func(t *testing.T) {
|
||||
query := QueryBuilderQuery[TraceAggregation]{
|
||||
SecondaryAggregations: []SecondaryAggregation{
|
||||
{
|
||||
Order: []OrderBy{
|
||||
{
|
||||
Key: OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: "value",
|
||||
FieldContext: telemetrytypes.FieldContextSpan,
|
||||
},
|
||||
},
|
||||
Direction: OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
GroupBy: []GroupByKey{
|
||||
{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: "region",
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
query.Normalize()
|
||||
|
||||
assert.Equal(t, "value", query.SecondaryAggregations[0].Order[0].Key.Name)
|
||||
assert.Equal(t, telemetrytypes.FieldContextSpan, query.SecondaryAggregations[0].Order[0].Key.FieldContext)
|
||||
assert.Equal(t, "region", query.SecondaryAggregations[0].GroupBy[0].Name)
|
||||
assert.Equal(t, telemetrytypes.FieldContextResource, query.SecondaryAggregations[0].GroupBy[0].FieldContext)
|
||||
})
|
||||
|
||||
t.Run("normalize with nil fields", func(t *testing.T) {
|
||||
query := QueryBuilderQuery[TraceAggregation]{
|
||||
Name: "A",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
}
|
||||
|
||||
// Should not panic
|
||||
query.Normalize()
|
||||
|
||||
assert.Equal(t, "A", query.Name)
|
||||
})
|
||||
|
||||
t.Run("normalize all fields together", func(t *testing.T) {
|
||||
query := QueryBuilderQuery[TraceAggregation]{
|
||||
SelectFields: []telemetrytypes.TelemetryFieldKey{
|
||||
{Name: "service.name", FieldContext: telemetrytypes.FieldContextResource},
|
||||
},
|
||||
GroupBy: []GroupByKey{
|
||||
{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: "host.name",
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
},
|
||||
},
|
||||
},
|
||||
Order: []OrderBy{
|
||||
{
|
||||
Key: OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: "duration",
|
||||
FieldContext: telemetrytypes.FieldContextSpan,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
SecondaryAggregations: []SecondaryAggregation{
|
||||
{
|
||||
Order: []OrderBy{
|
||||
{
|
||||
Key: OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: "count",
|
||||
FieldContext: telemetrytypes.FieldContextSpan,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
GroupBy: []GroupByKey{
|
||||
{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: "status.code",
|
||||
FieldContext: telemetrytypes.FieldContextSpan,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
query.Normalize()
|
||||
|
||||
assert.Equal(t, "service.name", query.SelectFields[0].Name)
|
||||
assert.Equal(t, "host.name", query.GroupBy[0].Name)
|
||||
assert.Equal(t, "duration", query.Order[0].Key.Name)
|
||||
assert.Equal(t, "count", query.SecondaryAggregations[0].Order[0].Key.Name)
|
||||
assert.Equal(t, "status.code", query.SecondaryAggregations[0].GroupBy[0].Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestQueryBuilderQuery_UnmarshalJSON(t *testing.T) {
|
||||
t.Run("valid trace query", func(t *testing.T) {
|
||||
jsonData := `{
|
||||
"name": "A",
|
||||
"signal": "traces",
|
||||
"stepInterval": 60,
|
||||
"aggregations": [{
|
||||
"expression": "count()",
|
||||
"alias": "trace_count"
|
||||
}],
|
||||
"filter": {
|
||||
"expression": "service.name = 'frontend'"
|
||||
},
|
||||
"groupBy": [{
|
||||
"name": "service.name",
|
||||
"fieldContext": "resource"
|
||||
}],
|
||||
"order": [{
|
||||
"key": {
|
||||
"name": "service.name",
|
||||
"fieldContext": "resource"
|
||||
},
|
||||
"direction": "desc"
|
||||
}],
|
||||
"limit": 100
|
||||
}`
|
||||
|
||||
var query QueryBuilderQuery[TraceAggregation]
|
||||
err := json.Unmarshal([]byte(jsonData), &query)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "A", query.Name)
|
||||
assert.Equal(t, telemetrytypes.SignalTraces, query.Signal)
|
||||
assert.Equal(t, int64(60000), query.StepInterval.Milliseconds())
|
||||
assert.Equal(t, 1, len(query.Aggregations))
|
||||
assert.Equal(t, "count()", query.Aggregations[0].Expression)
|
||||
assert.Equal(t, "trace_count", query.Aggregations[0].Alias)
|
||||
require.NotNil(t, query.Filter)
|
||||
assert.Equal(t, "service.name = 'frontend'", query.Filter.Expression)
|
||||
assert.Equal(t, 1, len(query.GroupBy))
|
||||
assert.Equal(t, "service.name", query.GroupBy[0].Name)
|
||||
assert.Equal(t, telemetrytypes.FieldContextResource, query.GroupBy[0].FieldContext)
|
||||
assert.Equal(t, 1, len(query.Order))
|
||||
assert.Equal(t, "service.name", query.Order[0].Key.Name)
|
||||
assert.Equal(t, telemetrytypes.FieldContextResource, query.Order[0].Key.FieldContext)
|
||||
assert.Equal(t, OrderDirectionDesc, query.Order[0].Direction)
|
||||
assert.Equal(t, 100, query.Limit)
|
||||
})
|
||||
|
||||
t.Run("valid metric query", func(t *testing.T) {
|
||||
jsonData := `{
|
||||
"name": "M",
|
||||
"signal": "metrics",
|
||||
"stepInterval": "1m",
|
||||
"aggregations": [{
|
||||
"metricName": "cpu_usage",
|
||||
"spaceAggregation": "avg",
|
||||
"timeAggregation": "avg"
|
||||
}]
|
||||
}`
|
||||
|
||||
var query QueryBuilderQuery[MetricAggregation]
|
||||
err := json.Unmarshal([]byte(jsonData), &query)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "M", query.Name)
|
||||
assert.Equal(t, telemetrytypes.SignalMetrics, query.Signal)
|
||||
assert.Equal(t, int64(60000), query.StepInterval.Milliseconds())
|
||||
assert.Equal(t, 1, len(query.Aggregations))
|
||||
assert.Equal(t, "cpu_usage", query.Aggregations[0].MetricName)
|
||||
})
|
||||
|
||||
t.Run("valid log query", func(t *testing.T) {
|
||||
jsonData := `{
|
||||
"name": "L",
|
||||
"signal": "logs",
|
||||
"aggregations": [{
|
||||
"expression": "count()",
|
||||
"alias": "log_count"
|
||||
}]
|
||||
}`
|
||||
|
||||
var query QueryBuilderQuery[LogAggregation]
|
||||
err := json.Unmarshal([]byte(jsonData), &query)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "L", query.Name)
|
||||
assert.Equal(t, telemetrytypes.SignalLogs, query.Signal)
|
||||
assert.Equal(t, 1, len(query.Aggregations))
|
||||
assert.Equal(t, "count()", query.Aggregations[0].Expression)
|
||||
})
|
||||
|
||||
t.Run("unknown field error", func(t *testing.T) {
|
||||
jsonData := `{
|
||||
"name": "A",
|
||||
"signal": "traces",
|
||||
"unknownField": "value"
|
||||
}`
|
||||
|
||||
var query QueryBuilderQuery[TraceAggregation]
|
||||
err := json.Unmarshal([]byte(jsonData), &query)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "unknown field")
|
||||
})
|
||||
|
||||
t.Run("query with all optional fields", func(t *testing.T) {
|
||||
// NOTE: This json payload is not realistic, just for testing all fields
|
||||
jsonData := `{
|
||||
"name": "A",
|
||||
"signal": "traces",
|
||||
"stepInterval": "5m",
|
||||
"source": "traces",
|
||||
"aggregations": [{
|
||||
"expression": "count()",
|
||||
"alias": "span.count"
|
||||
}],
|
||||
"disabled": true,
|
||||
"filter": {
|
||||
"expression": "service.name = 'api'"
|
||||
},
|
||||
"groupBy": [{
|
||||
"name": "service.name",
|
||||
"fieldContext": "resource"
|
||||
}],
|
||||
"order": [{
|
||||
"key": {
|
||||
"name": "timestamp",
|
||||
"fieldContext": "span"
|
||||
},
|
||||
"direction": "asc"
|
||||
}],
|
||||
"selectFields": [{
|
||||
"name": "trace_id",
|
||||
"fieldContext": "span"
|
||||
}],
|
||||
"limit": 50,
|
||||
"offset": 10,
|
||||
"cursor": "cursor123",
|
||||
"limitBy": {
|
||||
"value": "5",
|
||||
"keys": ["service.name"]
|
||||
},
|
||||
"having": {
|
||||
"expression": "count() > 10"
|
||||
},
|
||||
"secondaryAggregations": [{
|
||||
"limit": 20,
|
||||
"order": [{
|
||||
"key": {
|
||||
"name": "value",
|
||||
"fieldContext": "span"
|
||||
},
|
||||
"direction": "desc"
|
||||
}],
|
||||
"groupBy": [{
|
||||
"name": "region",
|
||||
"fieldContext": "resource"
|
||||
}]
|
||||
}],
|
||||
"functions": [{
|
||||
"name": "timeShift",
|
||||
"args": [{
|
||||
"name": "shift",
|
||||
"value": "1h"
|
||||
}]
|
||||
}],
|
||||
"legend": "{{service.name}}"
|
||||
}`
|
||||
|
||||
var query QueryBuilderQuery[TraceAggregation]
|
||||
err := json.Unmarshal([]byte(jsonData), &query)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "A", query.Name)
|
||||
assert.Equal(t, telemetrytypes.SignalTraces, query.Signal)
|
||||
assert.Equal(t, int64(300000), query.StepInterval.Milliseconds())
|
||||
// Source is set in the JSON, so it should be "traces", not SourceUnspecified
|
||||
assert.Equal(t, "traces", query.Source.String.StringValue())
|
||||
assert.True(t, query.Disabled)
|
||||
assert.Equal(t, query.Aggregations[0].Expression, "count()")
|
||||
assert.Equal(t, query.Aggregations[0].Alias, "span.count")
|
||||
assert.NotNil(t, query.Filter)
|
||||
assert.NotNil(t, query.GroupBy)
|
||||
assert.NotNil(t, query.Order)
|
||||
assert.NotNil(t, query.SelectFields)
|
||||
assert.Equal(t, 50, query.Limit)
|
||||
assert.Equal(t, 10, query.Offset)
|
||||
assert.Equal(t, "cursor123", query.Cursor)
|
||||
assert.NotNil(t, query.LimitBy)
|
||||
assert.NotNil(t, query.Having)
|
||||
assert.NotNil(t, query.SecondaryAggregations)
|
||||
assert.NotNil(t, query.Functions)
|
||||
assert.Equal(t, "{{service.name}}", query.Legend)
|
||||
})
|
||||
|
||||
t.Run("normalization happens during unmarshaling", func(t *testing.T) {
|
||||
jsonData := `{
|
||||
"name": "A",
|
||||
"signal": "traces",
|
||||
"selectFields": [{
|
||||
"name": "resource.service.name"
|
||||
}],
|
||||
"groupBy": [{
|
||||
"name": "resource.host.name"
|
||||
}],
|
||||
"order": [{
|
||||
"key": {
|
||||
"name": "span.duration"
|
||||
},
|
||||
"direction": "desc"
|
||||
}]
|
||||
}`
|
||||
|
||||
var query QueryBuilderQuery[TraceAggregation]
|
||||
err := json.Unmarshal([]byte(jsonData), &query)
|
||||
require.NoError(t, err)
|
||||
|
||||
// FieldContext should be normalized, Name should remain as-is
|
||||
assert.Equal(t, "service.name", query.SelectFields[0].Name)
|
||||
assert.Equal(t, telemetrytypes.FieldContextResource, query.SelectFields[0].FieldContext)
|
||||
assert.Equal(t, "host.name", query.GroupBy[0].Name)
|
||||
assert.Equal(t, telemetrytypes.FieldContextResource, query.GroupBy[0].FieldContext)
|
||||
assert.Equal(t, "duration", query.Order[0].Key.Name)
|
||||
assert.Equal(t, telemetrytypes.FieldContextSpan, query.Order[0].Key.FieldContext)
|
||||
})
|
||||
}
|
||||
61
pkg/types/querybuildertypes/querybuildertypesv5/cached.go
Normal file
61
pkg/types/querybuildertypes/querybuildertypesv5/cached.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package querybuildertypesv5
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"maps"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/cachetypes"
|
||||
)
|
||||
|
||||
var _ cachetypes.Cacheable = (*CachedData)(nil)
|
||||
|
||||
type CachedBucket struct {
|
||||
StartMs uint64 `json:"startMs"`
|
||||
EndMs uint64 `json:"endMs"`
|
||||
Type RequestType `json:"type"`
|
||||
Value json.RawMessage `json:"value"`
|
||||
Stats ExecStats `json:"stats"`
|
||||
}
|
||||
|
||||
func (c *CachedBucket) Clone() *CachedBucket {
|
||||
return &CachedBucket{
|
||||
StartMs: c.StartMs,
|
||||
EndMs: c.EndMs,
|
||||
Type: c.Type,
|
||||
Value: bytes.Clone(c.Value),
|
||||
Stats: ExecStats{
|
||||
RowsScanned: c.Stats.RowsScanned,
|
||||
BytesScanned: c.Stats.BytesScanned,
|
||||
DurationMS: c.Stats.DurationMS,
|
||||
StepIntervals: maps.Clone(c.Stats.StepIntervals),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CachedData represents the full cached data for a query
|
||||
type CachedData struct {
|
||||
Buckets []*CachedBucket `json:"buckets"`
|
||||
Warnings []string `json:"warnings"`
|
||||
}
|
||||
|
||||
func (c *CachedData) UnmarshalBinary(data []byte) error {
|
||||
return json.Unmarshal(data, c)
|
||||
}
|
||||
|
||||
func (c *CachedData) MarshalBinary() ([]byte, error) {
|
||||
return json.Marshal(c)
|
||||
}
|
||||
|
||||
func (c *CachedData) Clone() cachetypes.Cacheable {
|
||||
clonedCachedData := new(CachedData)
|
||||
clonedCachedData.Buckets = make([]*CachedBucket, len(c.Buckets))
|
||||
for i := range c.Buckets {
|
||||
clonedCachedData.Buckets[i] = c.Buckets[i].Clone()
|
||||
}
|
||||
|
||||
clonedCachedData.Warnings = make([]string, len(c.Warnings))
|
||||
copy(clonedCachedData.Warnings, c.Warnings)
|
||||
|
||||
return clonedCachedData
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package querybuildertypesv5
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func createBuckets_TimeSeries(numBuckets int) []*CachedBucket {
|
||||
buckets := make([]*CachedBucket, numBuckets)
|
||||
|
||||
for i := 0; i < numBuckets; i++ {
|
||||
startMs := uint64(i * 10000)
|
||||
endMs := uint64((i + 1) * 10000)
|
||||
|
||||
timeSeriesData := &TimeSeriesData{
|
||||
QueryName: "A",
|
||||
Aggregations: []*AggregationBucket{
|
||||
{
|
||||
Index: 0,
|
||||
Series: []*TimeSeries{
|
||||
{
|
||||
Labels: []*Label{
|
||||
{Key: telemetrytypes.TelemetryFieldKey{Name: "service"}, Value: "test"},
|
||||
},
|
||||
Values: []*TimeSeriesValue{
|
||||
{Timestamp: 1672563720000, Value: 1, Partial: true}, // 12:02
|
||||
{Timestamp: 1672563900000, Value: 2}, // 12:05
|
||||
{Timestamp: 1672564200000, Value: 2.5}, // 12:10
|
||||
{Timestamp: 1672564500000, Value: 2.6}, // 12:15
|
||||
{Timestamp: 1672566600000, Value: 2.9}, // 12:50
|
||||
{Timestamp: 1672566900000, Value: 3}, // 12:55
|
||||
{Timestamp: 1672567080000, Value: 4, Partial: true}, // 12:58
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
value, err := json.Marshal(timeSeriesData)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
buckets[i] = &CachedBucket{
|
||||
StartMs: startMs,
|
||||
EndMs: endMs,
|
||||
Type: RequestTypeTimeSeries,
|
||||
Value: json.RawMessage(value),
|
||||
Stats: ExecStats{
|
||||
RowsScanned: uint64(i * 500),
|
||||
BytesScanned: uint64(i * 10000),
|
||||
DurationMS: uint64(i * 1000),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return buckets
|
||||
}
|
||||
|
||||
func BenchmarkCachedData_JSONMarshal_10kbuckets(b *testing.B) {
|
||||
buckets := createBuckets_TimeSeries(10000)
|
||||
data := &CachedData{Buckets: buckets}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := json.Marshal(data)
|
||||
assert.NoError(b, err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCachedData_Clone_10kbuckets(b *testing.B) {
|
||||
buckets := createBuckets_TimeSeries(10000)
|
||||
data := &CachedData{Buckets: buckets}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = data.Clone()
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,43 @@ func (f *QueryBuilderFormula) UnmarshalJSON(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate checks if the QueryBuilderFormula fields are valid
|
||||
func (f QueryBuilderFormula) Validate() error {
|
||||
// Validate name is not blank
|
||||
if strings.TrimSpace(f.Name) == "" {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"formula name cannot be blank",
|
||||
)
|
||||
}
|
||||
|
||||
// Validate expression is not blank
|
||||
if strings.TrimSpace(f.Expression) == "" {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"formula expression cannot be blank",
|
||||
)
|
||||
}
|
||||
|
||||
// Validate functions if present
|
||||
for i, fn := range f.Functions {
|
||||
if err := fn.Validate(); err != nil {
|
||||
fnId := fmt.Sprintf("function #%d", i+1)
|
||||
if f.Name != "" {
|
||||
fnId = fmt.Sprintf("function #%d in formula '%s'", i+1, f.Name)
|
||||
}
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"invalid %s: %s",
|
||||
fnId,
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// small container to store the query name and index or alias reference
|
||||
// for a variable in the formula expression
|
||||
// read below for more details on aggregation references
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package querybuildertypesv5
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
@@ -33,6 +36,46 @@ var (
|
||||
FunctionNameFillZero = FunctionName{valuer.NewString("fillZero")}
|
||||
)
|
||||
|
||||
// Validate checks if the FunctionName is valid and one of the known types
|
||||
func (fn FunctionName) Validate() error {
|
||||
validFunctions := []FunctionName{
|
||||
FunctionNameCutOffMin,
|
||||
FunctionNameCutOffMax,
|
||||
FunctionNameClampMin,
|
||||
FunctionNameClampMax,
|
||||
FunctionNameAbsolute,
|
||||
FunctionNameRunningDiff,
|
||||
FunctionNameLog2,
|
||||
FunctionNameLog10,
|
||||
FunctionNameCumulativeSum,
|
||||
FunctionNameEWMA3,
|
||||
FunctionNameEWMA5,
|
||||
FunctionNameEWMA7,
|
||||
FunctionNameMedian3,
|
||||
FunctionNameMedian5,
|
||||
FunctionNameMedian7,
|
||||
FunctionNameTimeShift,
|
||||
FunctionNameAnomaly,
|
||||
FunctionNameFillZero,
|
||||
}
|
||||
|
||||
if slices.Contains(validFunctions, fn) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Format valid functions as comma-separated string
|
||||
var validFunctionNames []string
|
||||
for _, fn := range validFunctions {
|
||||
validFunctionNames = append(validFunctionNames, fn.StringValue())
|
||||
}
|
||||
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"invalid function name: %s",
|
||||
fn.StringValue(),
|
||||
).WithAdditional(fmt.Sprintf("valid functions are: %s", strings.Join(validFunctionNames, ", ")))
|
||||
}
|
||||
|
||||
// ApplyFunction applies the given function to the result data
|
||||
func ApplyFunction(fn Function, result *TimeSeries) *TimeSeries {
|
||||
// Extract the function name and arguments
|
||||
@@ -112,6 +155,61 @@ func ApplyFunction(fn Function, result *TimeSeries) *TimeSeries {
|
||||
return result
|
||||
}
|
||||
|
||||
// ValidateArgs validates the arguments for the given function
|
||||
func (fn Function) ValidateArgs() error {
|
||||
// Extract the function name and arguments
|
||||
name := fn.Name
|
||||
args := fn.Args
|
||||
|
||||
switch name {
|
||||
case FunctionNameCutOffMin, FunctionNameCutOffMax, FunctionNameClampMin, FunctionNameClampMax:
|
||||
if len(args) == 0 {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"threshold value is required for function %s",
|
||||
name.StringValue(),
|
||||
)
|
||||
}
|
||||
_, err := parseFloat64Arg(args[0].Value)
|
||||
if err != nil {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"threshold value must be a floating value for function %s",
|
||||
name.StringValue(),
|
||||
)
|
||||
}
|
||||
case FunctionNameEWMA3, FunctionNameEWMA5, FunctionNameEWMA7:
|
||||
if len(args) == 0 {
|
||||
return nil // alpha is optional for EWMA functions
|
||||
}
|
||||
_, err := parseFloat64Arg(args[0].Value)
|
||||
if err != nil {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"alpha value must be a floating value for function %s",
|
||||
name.StringValue(),
|
||||
)
|
||||
}
|
||||
case FunctionNameTimeShift:
|
||||
if len(args) == 0 {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"time shift value is required for function %s",
|
||||
name.StringValue(),
|
||||
)
|
||||
}
|
||||
_, err := parseFloat64Arg(args[0].Value)
|
||||
if err != nil {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"time shift value must be a floating value for function %s",
|
||||
name.StringValue(),
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseFloat64Arg parses an argument to float64
|
||||
func parseFloat64Arg(value any) (float64, error) {
|
||||
switch v := value.(type) {
|
||||
|
||||
@@ -47,19 +47,19 @@ func (q *QueryEnvelope) UnmarshalJSON(data []byte) error {
|
||||
switch header.Signal {
|
||||
case telemetrytypes.SignalTraces:
|
||||
var spec QueryBuilderQuery[TraceAggregation]
|
||||
if err := UnmarshalJSONWithContext(shadow.Spec, &spec, "query spec"); err != nil {
|
||||
if err := json.Unmarshal(shadow.Spec, &spec); err != nil {
|
||||
return wrapUnmarshalError(err, "invalid trace builder query spec: %v", err)
|
||||
}
|
||||
q.Spec = spec
|
||||
case telemetrytypes.SignalLogs:
|
||||
var spec QueryBuilderQuery[LogAggregation]
|
||||
if err := UnmarshalJSONWithContext(shadow.Spec, &spec, "query spec"); err != nil {
|
||||
if err := json.Unmarshal(shadow.Spec, &spec); err != nil {
|
||||
return wrapUnmarshalError(err, "invalid log builder query spec: %v", err)
|
||||
}
|
||||
q.Spec = spec
|
||||
case telemetrytypes.SignalMetrics:
|
||||
var spec QueryBuilderQuery[MetricAggregation]
|
||||
if err := UnmarshalJSONWithContext(shadow.Spec, &spec, "query spec"); err != nil {
|
||||
if err := json.Unmarshal(shadow.Spec, &spec); err != nil {
|
||||
return wrapUnmarshalError(err, "invalid metric builder query spec: %v", err)
|
||||
}
|
||||
q.Spec = spec
|
||||
@@ -75,6 +75,7 @@ func (q *QueryEnvelope) UnmarshalJSON(data []byte) error {
|
||||
|
||||
case QueryTypeFormula:
|
||||
var spec QueryBuilderFormula
|
||||
// TODO: use json.Unmarshal here after implementing custom unmarshaler for QueryBuilderFormula
|
||||
if err := UnmarshalJSONWithContext(shadow.Spec, &spec, "formula spec"); err != nil {
|
||||
return wrapUnmarshalError(err, "invalid formula spec: %v", err)
|
||||
}
|
||||
@@ -82,6 +83,7 @@ func (q *QueryEnvelope) UnmarshalJSON(data []byte) error {
|
||||
|
||||
case QueryTypeJoin:
|
||||
var spec QueryBuilderJoin
|
||||
// TODO: use json.Unmarshal here after implementing custom unmarshaler for QueryBuilderJoin
|
||||
if err := UnmarshalJSONWithContext(shadow.Spec, &spec, "join spec"); err != nil {
|
||||
return wrapUnmarshalError(err, "invalid join spec: %v", err)
|
||||
}
|
||||
@@ -89,13 +91,14 @@ func (q *QueryEnvelope) UnmarshalJSON(data []byte) error {
|
||||
|
||||
case QueryTypeTraceOperator:
|
||||
var spec QueryBuilderTraceOperator
|
||||
if err := UnmarshalJSONWithContext(shadow.Spec, &spec, "trace operator spec"); err != nil {
|
||||
if err := json.Unmarshal(shadow.Spec, &spec); err != nil {
|
||||
return wrapUnmarshalError(err, "invalid trace operator spec: %v", err)
|
||||
}
|
||||
q.Spec = spec
|
||||
|
||||
case QueryTypePromQL:
|
||||
var spec PromQuery
|
||||
// TODO: use json.Unmarshal here after implementing custom unmarshaler for PromQuery
|
||||
if err := UnmarshalJSONWithContext(shadow.Spec, &spec, "PromQL spec"); err != nil {
|
||||
return wrapUnmarshalError(err, "invalid PromQL spec: %v", err)
|
||||
}
|
||||
@@ -103,6 +106,7 @@ func (q *QueryEnvelope) UnmarshalJSON(data []byte) error {
|
||||
|
||||
case QueryTypeClickHouseSQL:
|
||||
var spec ClickHouseQuery
|
||||
// TODO: use json.Unmarshal here after implementing custom unmarshaler for ClickHouseQuery
|
||||
if err := UnmarshalJSONWithContext(shadow.Spec, &spec, "ClickHouse SQL spec"); err != nil {
|
||||
return wrapUnmarshalError(err, "invalid ClickHouse SQL spec: %v", err)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user