Compare commits

...

6 Commits

Author SHA1 Message Date
Ashwin Bhatkal
c74e650246 fix: lint errors 2026-01-15 20:19:46 +05:30
Ashwin Bhatkal
a178c1b266 chore: remove airbnb from ESLint, update prettier rules and update VS Code settings 2026-01-15 19:28:08 +05:30
Abhi kumar
8b9fcae0cb fix: added layout fix for changelog modal (#10012) 2026-01-15 12:30:10 +00:00
Abhi kumar
e75abcb108 Fix/animation offload to cpu (#9961)
* fix: transition is getting offloaded to cpu because gpu can't handle it in composition

* chore: changed transition from all to transform only
2026-01-15 13:43:44 +05:30
primus-bot[bot]
ffa15f9ec1 chore(release): bump to v0.107.0 (#10007)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2026-01-15 12:18:52 +05:30
Ishan
87562f5387 feat: onGroupBy - show timeseries panel - #3403 (#9921)
* feat: onGroupBy - show timeseries

* feat: updated testcases for tableview actions

* feat: updated pipeline file to remove groupBy as not used

* feat: updated better code checks

* feat: updated better code checks

* feat: updated tableview actions to update query

* feat: updated types and test cases

* feat: updated dataType (noramlized)
2026-01-15 08:53:08 +05:30
38 changed files with 330 additions and 107 deletions

2
.gitignore vendored
View File

@@ -12,7 +12,6 @@ frontend/coverage
# production
frontend/build
frontend/.vscode
frontend/.yarnclean
frontend/.temp_cache
frontend/test-results
@@ -31,7 +30,6 @@ frontend/src/constants/env.ts
.idea
**/.vscode
**/build
**/storage
**/locust-scripts/__pycache__/

9
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,9 @@
{
"eslint.workingDirectories": ["./frontend"],
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"prettier.requireConfig": true
}

View File

@@ -176,7 +176,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.106.0
image: signoz/signoz:v0.107.0
command:
- --config=/root/config/prometheus.yml
ports:

View File

@@ -117,7 +117,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.106.0
image: signoz/signoz:v0.107.0
command:
- --config=/root/config/prometheus.yml
ports:

View File

@@ -179,7 +179,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.106.0}
image: signoz/signoz:${VERSION:-v0.107.0}
container_name: signoz
command:
- --config=/root/config/prometheus.yml

View File

@@ -111,7 +111,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.106.0}
image: signoz/signoz:${VERSION:-v0.107.0}
container_name: signoz
command:
- --config=/root/config/prometheus.yml

View File

@@ -7,8 +7,6 @@ module.exports = {
'jest/globals': true,
},
extends: [
'airbnb',
'airbnb-typescript',
'eslint:recommended',
'plugin:react/recommended',
'plugin:@typescript-eslint/recommended',
@@ -35,6 +33,7 @@ module.exports = {
'react-hooks',
'prettier',
'jest',
'jsx-a11y',
],
settings: {
react: {
@@ -72,9 +71,6 @@ module.exports = {
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error',
// airbnb
'no-underscore-dangle': 'off',
'no-console': 'off',
'import/prefer-default-export': 'off',
'import/extensions': [
'error',
@@ -87,6 +83,9 @@ module.exports = {
},
],
'import/no-extraneous-dependencies': ['error', { devDependencies: true }],
// Disabled because TypeScript already handles this check more accurately,
// and the rule has false positives with type-only imports (e.g., TooltipProps from antd)
'import/named': 'off',
'no-plusplus': 'off',
'jsx-a11y/label-has-associated-control': [
'error',
@@ -104,7 +103,10 @@ module.exports = {
},
},
],
'@typescript-eslint/no-unused-vars': 'error',
// Allow empty functions for mocks, default context values, and noop callbacks
'@typescript-eslint/no-empty-function': 'off',
// Allow underscore prefix for intentionally unused variables (e.g., const { id: _id, ...rest } = props)
'@typescript-eslint/no-unused-vars': 'warn',
'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
'arrow-body-style': ['error', 'as-needed'],

View File

@@ -4,5 +4,14 @@
"tabWidth": 1,
"singleQuote": true,
"jsxSingleQuote": false,
"semi": true
"semi": true,
"printWidth": 80,
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "always",
"endOfLine": "lf",
"quoteProps": "as-needed",
"proseWrap": "preserve",
"htmlWhitespaceSensitivity": "css",
"embeddedLanguageFormatting": "auto"
}

8
frontend/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,8 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"prettier.requireConfig": true
}

View File

@@ -34,12 +34,18 @@ Embrace the spirit of collaboration and contribute to the success of our open-so
### Linting and Setup
- It is crucial to refrain from disabling ESLint and TypeScript errors within the project. If there is a specific rule that needs to be disabled, provide a clear and justified explanation for doing so. Maintaining the integrity of the linting and type-checking processes ensures code quality and consistency throughout the codebase.
- In our project, we rely on several essential ESLint plugins, namely:
- [plugin:@typescript-eslint](https://typescript-eslint.io/rules/)
- [airbnb styleguide](https://github.com/airbnb/javascript)
- [plugin:sonarjs](https://github.com/SonarSource/eslint-plugin-sonarjs)
- In our project, we rely on several essential ESLint plugins and configurations:
To ensure compliance with our coding standards and best practices, we encourage you to refer to the documentation of these plugins. Familiarizing yourself with the ESLint rules they provide will help maintain code quality and consistency throughout the project.
- [eslint:recommended](https://eslint.org/docs/latest/rules/) - Core ESLint rules for JavaScript best practices
- [plugin:@typescript-eslint](https://typescript-eslint.io/rules/) - TypeScript-specific linting rules
- [plugin:react](https://github.com/jsx-eslint/eslint-plugin-react) - React best practices and patterns
- [plugin:react-hooks](https://www.npmjs.com/package/eslint-plugin-react-hooks) - Rules of Hooks enforcement
- [plugin:sonarjs](https://github.com/SonarSource/eslint-plugin-sonarjs) - Code quality and complexity analysis
- [plugin:prettier](https://github.com/prettier/eslint-plugin-prettier) - Code formatting via Prettier
- [simple-import-sort](https://github.com/lydell/eslint-plugin-simple-import-sort) - Automatic import organization
- [plugin:jsx-a11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y) - Accessibility rules for JSX elements
To ensure compliance with our coding standards and best practices, we encourage you to refer to the documentation of these plugins. Familiarizing yourself with the ESLint rules they provide will help maintain code quality and consistency throughout the project.
### Naming Conventions

View File

@@ -219,16 +219,11 @@
"compression-webpack-plugin": "9.0.0",
"copy-webpack-plugin": "^11.0.0",
"eslint": "^7.32.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^16.1.4",
"eslint-config-prettier": "^8.3.0",
"eslint-config-standard": "^16.0.3",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-jest": "^26.9.0",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-react": "^7.24.0",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-simple-import-sort": "^7.0.0",

View File

@@ -58,6 +58,7 @@
flex-direction: column;
gap: 16px;
padding-left: 30px;
margin-bottom: 1rem;
li {
position: relative;

View File

@@ -1,4 +1,5 @@
import { Chart, ChartConfiguration, ChartData, Color } from 'chart.js';
// eslint-disable-next-line import/namespace -- side-effect import that registers Chart.js date adapter
import * as chartjsAdapter from 'chartjs-adapter-date-fns';
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';

View File

@@ -1,16 +1,16 @@
import { DrawerProps } from 'antd';
import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { ActionItemProps } from 'container/LogDetailedView/ActionItem';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { VIEWS } from './constants';
export type LogDetailProps = {
log: ILog | null;
selectedTab: VIEWS;
onGroupByAttribute?: (fieldKey: string, dataType?: DataTypes) => Promise<void>;
handleChangeSelectedView?: ChangeViewFunctionType;
isListViewPanel?: boolean;
listViewPanelSelectedFields?: IField[] | null;
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &

View File

@@ -55,11 +55,11 @@ function LogDetailInner({
log,
onClose,
onAddToQuery,
onGroupByAttribute,
onClickActionItem,
selectedTab,
isListViewPanel = false,
listViewPanelSelectedFields,
handleChangeSelectedView,
}: LogDetailInnerProps): JSX.Element {
const initialContextQuery = useInitialQuery(log);
const [contextQuery, setContextQuery] = useState<Query | undefined>(
@@ -365,10 +365,10 @@ function LogDetailInner({
logData={log}
onAddToQuery={onAddToQuery}
onClickActionItem={onClickActionItem}
onGroupByAttribute={onGroupByAttribute}
isListViewPanel={isListViewPanel}
selectedOptions={options}
listViewPanelSelectedFields={listViewPanelSelectedFields}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
{selectedView === VIEW_TYPES.JSON && <JSONView logData={log} />}

View File

@@ -6,6 +6,7 @@ import cx from 'classnames';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { getSanitizedLogBody } from 'container/LogDetailedView/utils';
import { FontSize } from 'container/OptionsMenu/types';
import { useActiveLog } from 'hooks/logs/useActiveLog';
@@ -108,6 +109,7 @@ type ListLogViewProps = {
activeLog?: ILog | null;
linesPerRow: number;
fontSize: FontSize;
handleChangeSelectedView?: ChangeViewFunctionType;
};
function ListLogView({
@@ -118,6 +120,7 @@ function ListLogView({
activeLog,
linesPerRow,
fontSize,
handleChangeSelectedView,
}: ListLogViewProps): JSX.Element {
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
@@ -131,7 +134,6 @@ function ListLogView({
onAddToQuery: handleAddToQuery,
onSetActiveLog: handleSetActiveContextLog,
onClearActiveLog: handleClearActiveContextLog,
onGroupByAttribute,
} = useActiveLog();
const isDarkMode = useIsDarkMode();
@@ -255,7 +257,7 @@ function ListLogView({
onAddToQuery={handleAddToQuery}
selectedTab={VIEW_TYPES.CONTEXT}
onClose={handlerClearActiveContextLog}
onGroupByAttribute={onGroupByAttribute}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
</>
@@ -264,6 +266,7 @@ function ListLogView({
ListLogView.defaultProps = {
activeLog: null,
handleChangeSelectedView: undefined,
};
LogGeneralField.defaultProps = {

View File

@@ -39,6 +39,7 @@ function RawLogView({
selectedFields = [],
fontSize,
onLogClick,
handleChangeSelectedView,
}: RawLogViewProps): JSX.Element {
const {
isHighlighted: isUrlHighlighted,
@@ -52,7 +53,6 @@ function RawLogView({
onSetActiveLog,
onClearActiveLog,
onAddToQuery,
onGroupByAttribute,
} = useActiveLog();
const [selectedTab, setSelectedTab] = useState<VIEWS | undefined>();
@@ -224,13 +224,12 @@ function RawLogView({
onClose={handleCloseLogDetail}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
onGroupByAttribute={onGroupByAttribute}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
</RawLogViewContainer>
);
}
RawLogView.defaultProps = {
isActiveLog: false,
isReadOnly: false,

View File

@@ -1,3 +1,4 @@
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { FontSize } from 'container/OptionsMenu/types';
import { MouseEvent } from 'react';
import { IField } from 'types/api/logs/fields';
@@ -14,6 +15,7 @@ export interface RawLogViewProps {
fontSize: FontSize;
selectedFields?: IField[];
onLogClick?: (log: ILog, event: MouseEvent) => void;
handleChangeSelectedView?: ChangeViewFunctionType;
}
export interface RawLogContentProps {

View File

@@ -4,6 +4,7 @@ import { Switch, Typography } from 'antd';
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
import { MAX_LOGS_LIST_SIZE } from 'constants/liveTail';
import { LOCALSTORAGE } from 'constants/localStorage';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import GoToTop from 'container/GoToTop';
import { useOptionsMenu } from 'container/OptionsMenu';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
@@ -21,7 +22,13 @@ import { ILiveLogsLog } from '../LiveLogsList/types';
import LiveLogsListChart from '../LiveLogsListChart';
import { QueryHistoryState } from '../types';
function LiveLogsContainer(): JSX.Element {
interface LiveLogsContainerProps {
handleChangeSelectedView?: ChangeViewFunctionType;
}
function LiveLogsContainer({
handleChangeSelectedView,
}: LiveLogsContainerProps): JSX.Element {
const location = useLocation();
const [logs, setLogs] = useState<ILiveLogsLog[]>([]);
const { currentQuery, stagedQuery } = useQueryBuilder();
@@ -247,6 +254,7 @@ function LiveLogsContainer(): JSX.Element {
<LiveLogsList
logs={logs}
isLoading={initialLoading && logs.length === 0}
handleChangeSelectedView={handleChangeSelectedView}
/>
</div>
</div>
@@ -256,4 +264,8 @@ function LiveLogsContainer(): JSX.Element {
);
}
LiveLogsContainer.defaultProps = {
handleChangeSelectedView: undefined,
};
export default LiveLogsContainer;

View File

@@ -25,7 +25,11 @@ import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { LiveLogsListProps } from './types';
function LiveLogsList({ logs, isLoading }: LiveLogsListProps): JSX.Element {
function LiveLogsList({
logs,
isLoading,
handleChangeSelectedView,
}: LiveLogsListProps): JSX.Element {
const ref = useRef<VirtuosoHandle>(null);
const { isConnectionLoading } = useEventSource();
@@ -36,7 +40,6 @@ function LiveLogsList({ logs, isLoading }: LiveLogsListProps): JSX.Element {
activeLog,
onClearActiveLog,
onAddToQuery,
onGroupByAttribute,
onSetActiveLog,
} = useActiveLog();
@@ -72,6 +75,7 @@ function LiveLogsList({ logs, isLoading }: LiveLogsListProps): JSX.Element {
linesPerRow={options.maxLines}
selectedFields={selectedFields}
fontSize={options.fontSize}
handleChangeSelectedView={handleChangeSelectedView}
/>
);
}
@@ -85,10 +89,12 @@ function LiveLogsList({ logs, isLoading }: LiveLogsListProps): JSX.Element {
onAddToQuery={onAddToQuery}
onSetActiveLog={onSetActiveLog}
fontSize={options.fontSize}
handleChangeSelectedView={handleChangeSelectedView}
/>
);
},
[
handleChangeSelectedView,
onAddToQuery,
onSetActiveLog,
options.fontSize,
@@ -147,6 +153,7 @@ function LiveLogsList({ logs, isLoading }: LiveLogsListProps): JSX.Element {
appendTo: 'end',
activeLogIndex,
}}
handleChangeSelectedView={handleChangeSelectedView}
/>
) : (
<Card style={{ width: '100%' }} bodyStyle={CARD_BODY_STYLE}>
@@ -170,12 +177,11 @@ function LiveLogsList({ logs, isLoading }: LiveLogsListProps): JSX.Element {
log={activeLog}
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onGroupByAttribute={onGroupByAttribute}
onClickActionItem={onAddToQuery}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
</div>
);
}
export default memo(LiveLogsList);

View File

@@ -1,3 +1,4 @@
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { ILog } from 'types/api/logs/log';
export interface ILiveLogsLog {
@@ -8,4 +9,5 @@ export interface ILiveLogsLog {
export type LiveLogsListProps = {
logs: ILiveLogsLog[];
isLoading: boolean;
handleChangeSelectedView?: ChangeViewFunctionType;
};

View File

@@ -12,13 +12,13 @@ import {
Typography,
} from 'antd';
import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { OptionsQuery } from 'container/OptionsMenu/types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { ChevronDown, ChevronRight, Search } from 'lucide-react';
import { ReactNode, useState } from 'react';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { ActionItemProps } from './ActionItem';
import TableView from './TableView';
@@ -29,7 +29,7 @@ interface OverviewProps {
isListViewPanel?: boolean;
selectedOptions: OptionsQuery;
listViewPanelSelectedFields?: IField[] | null;
onGroupByAttribute?: (fieldKey: string, dataType?: DataTypes) => Promise<void>;
handleChangeSelectedView?: ChangeViewFunctionType;
}
type Props = OverviewProps &
@@ -42,8 +42,8 @@ function Overview({
onClickActionItem,
isListViewPanel = false,
selectedOptions,
onGroupByAttribute,
listViewPanelSelectedFields,
handleChangeSelectedView,
}: Props): JSX.Element {
const [isWrapWord, setIsWrapWord] = useState<boolean>(true);
const [isSearchVisible, setIsSearchVisible] = useState<boolean>(false);
@@ -208,11 +208,11 @@ function Overview({
logData={logData}
onAddToQuery={onAddToQuery}
fieldSearchInput={fieldSearchInput}
onGroupByAttribute={onGroupByAttribute}
onClickActionItem={onClickActionItem}
isListViewPanel={isListViewPanel}
selectedOptions={selectedOptions}
listViewPanelSelectedFields={listViewPanelSelectedFields}
handleChangeSelectedView={handleChangeSelectedView}
/>
</>
),
@@ -227,7 +227,7 @@ function Overview({
Overview.defaultProps = {
isListViewPanel: false,
listViewPanelSelectedFields: null,
onGroupByAttribute: undefined,
handleChangeSelectedView: undefined,
};
export default Overview;

View File

@@ -13,6 +13,7 @@ import AddToQueryHOC, {
import { ResizeTable } from 'components/ResizeTable';
import { OPERATORS } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
import { MetricsType } from 'container/MetricsApplication/constant';
import { FontSize, OptionsQuery } from 'container/OptionsMenu/types';
@@ -47,7 +48,7 @@ interface TableViewProps {
selectedOptions: OptionsQuery;
isListViewPanel?: boolean;
listViewPanelSelectedFields?: IField[] | null;
onGroupByAttribute?: (fieldKey: string, dataType?: DataTypes) => Promise<void>;
handleChangeSelectedView?: ChangeViewFunctionType;
}
type Props = TableViewProps &
@@ -61,8 +62,8 @@ function TableView({
onClickActionItem,
isListViewPanel = false,
selectedOptions,
onGroupByAttribute,
listViewPanelSelectedFields,
handleChangeSelectedView,
}: Props): JSX.Element | null {
const dispatch = useDispatch<Dispatch<AppActions>>();
const [isfilterInLoading, setIsFilterInLoading] = useState<boolean>(false);
@@ -295,7 +296,7 @@ function TableView({
isfilterInLoading={isfilterInLoading}
isfilterOutLoading={isfilterOutLoading}
onClickHandler={onClickHandler}
onGroupByAttribute={onGroupByAttribute}
handleChangeSelectedView={handleChangeSelectedView}
/>
),
},
@@ -338,7 +339,7 @@ function TableView({
TableView.defaultProps = {
isListViewPanel: false,
listViewPanelSelectedFields: null,
onGroupByAttribute: undefined,
handleChangeSelectedView: undefined,
};
export interface DataType {

View File

@@ -7,15 +7,24 @@ import GroupByIcon from 'assets/CustomIcons/GroupByIcon';
import cx from 'classnames';
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { QueryParams } from 'constants/query';
import { OPERATORS } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
import { MetricsType } from 'container/MetricsApplication/constant';
import { useGetSearchQueryParam } from 'hooks/queryBuilder/useGetSearchQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { ICurrentQueryData } from 'hooks/useHandleExplorerTabChange';
import { ArrowDownToDot, ArrowUpFromDot, Ellipsis } from 'lucide-react';
import { ExplorerViews } from 'pages/LogsExplorer/utils';
import { useTimezone } from 'providers/Timezone';
import React, { useCallback, useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
BaseAutocompleteData,
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataType } from '../TableView';
import {
@@ -33,7 +42,6 @@ interface ITableViewActionsProps {
isListViewPanel: boolean;
isfilterInLoading: boolean;
isfilterOutLoading: boolean;
onGroupByAttribute?: (fieldKey: string, dataType?: DataTypes) => Promise<void>;
onClickHandler: (
operator: string,
fieldKey: string,
@@ -41,6 +49,7 @@ interface ITableViewActionsProps {
dataType: string | undefined,
logType: MetricsType | undefined,
) => () => void;
handleChangeSelectedView?: ChangeViewFunctionType;
}
// Memoized Tree Component
@@ -118,10 +127,12 @@ export default function TableViewActions(
isfilterInLoading,
isfilterOutLoading,
onClickHandler,
onGroupByAttribute,
handleChangeSelectedView,
} = props;
const { pathname } = useLocation();
const { stagedQuery, updateQueriesData } = useQueryBuilder();
const viewName = useGetSearchQueryParam(QueryParams.viewName) || '';
const { dataType, logType: fieldType } = getFieldAttributes(record.field);
// there is no option for where clause in old logs explorer and live logs page
@@ -145,6 +156,42 @@ export default function TableViewActions(
const fieldFilterKey = filterKeyForField(fieldData.field);
const handleGroupByAttribute = useCallback((): void => {
if (!stagedQuery) return;
const normalizedDataType: DataTypes | undefined =
dataType && Object.values(DataTypes).includes(dataType as DataTypes)
? (dataType as DataTypes)
: undefined;
const updatedQuery = updateQueriesData(stagedQuery, 'queryData', (item) => {
const newGroupByItem: BaseAutocompleteData = {
key: fieldFilterKey,
type: fieldType || '',
dataType: normalizedDataType,
};
const updatedGroupBy = [...(item.groupBy || []), newGroupByItem];
return { ...item, groupBy: updatedGroupBy };
});
const queryData: ICurrentQueryData = {
name: viewName,
id: updatedQuery.id,
query: updatedQuery,
};
handleChangeSelectedView?.(ExplorerViews.TIMESERIES, queryData);
}, [
stagedQuery,
updateQueriesData,
fieldFilterKey,
fieldType,
dataType,
handleChangeSelectedView,
viewName,
]);
// Memoize textToCopy computation
const textToCopy = useMemo(() => {
let text = fieldData.value;
@@ -268,9 +315,7 @@ export default function TableViewActions(
className="group-by-clause"
type="text"
icon={<GroupByIcon />}
onClick={(): Promise<void> | void =>
onGroupByAttribute?.(fieldFilterKey)
}
onClick={handleGroupByAttribute}
>
Group By Attribute
</Button>
@@ -348,9 +393,7 @@ export default function TableViewActions(
className="group-by-clause"
type="text"
icon={<GroupByIcon />}
onClick={(): Promise<void> | void =>
onGroupByAttribute?.(fieldFilterKey)
}
onClick={handleGroupByAttribute}
>
Group By Attribute
</Button>
@@ -373,5 +416,5 @@ export default function TableViewActions(
}
TableViewActions.defaultProps = {
onGroupByAttribute: undefined,
handleChangeSelectedView: undefined,
};

View File

@@ -1,5 +1,8 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
import { useGetSearchQueryParam } from 'hooks/queryBuilder/useGetSearchQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { ExplorerViews } from 'pages/LogsExplorer/utils';
import TableViewActions from '../TableViewActions';
import useAsyncJSONProcessing from '../useAsyncJSONProcessing';
@@ -49,6 +52,20 @@ jest.mock('../useAsyncJSONProcessing', () => ({
default: jest.fn(),
}));
jest.mock('antd', () => {
const antd = jest.requireActual('antd');
return {
...antd,
// Render popover content inline to make its children testable
Popover: ({ content, children }: any): JSX.Element => (
<div data-testid="popover">
<div data-testid="popover-content">{content}</div>
{children}
</div>
),
};
});
jest.mock('providers/Timezone', () => ({
useTimezone: (): {
formatTimezoneAdjustedTimestamp: (timestamp: string) => string;
@@ -71,29 +88,35 @@ jest.mock('react-router-dom', () => ({
}),
}));
jest.mock('hooks/queryBuilder/useQueryBuilder');
jest.mock('hooks/queryBuilder/useGetSearchQueryParam');
describe('TableViewActions', () => {
const TEST_VALUE = 'test value';
const TEST_FIELD = 'test-field';
const ACTION_BUTTON_TEST_ID = '.action-btn';
const defaultProps = {
fieldData: {
field: 'test-field',
field: TEST_FIELD,
value: TEST_VALUE,
},
record: {
key: 'test-key',
field: 'test-field',
field: TEST_FIELD,
value: TEST_VALUE,
},
isListViewPanel: false,
isfilterInLoading: false,
isfilterOutLoading: false,
onClickHandler: jest.fn(),
onGroupByAttribute: jest.fn(),
handleChangeSelectedView: jest.fn(),
};
beforeEach(() => {
mockCopyToClipboard = jest.fn();
mockNotificationsSuccess = jest.fn();
defaultProps.onClickHandler = jest.fn();
defaultProps.handleChangeSelectedView = jest.fn();
// Default mock for useAsyncJSONProcessing
const mockUseAsyncJSONProcessing = jest.mocked(useAsyncJSONProcessing);
@@ -102,6 +125,24 @@ describe('TableViewActions', () => {
treeData: null,
error: null,
});
// Default mock for useQueryBuilder
jest.mocked(useQueryBuilder).mockReturnValue({
stagedQuery: null,
updateQueriesData: jest.fn((query, type, callback) => {
const updatedBuilder = {
...query.builder,
[type]: query.builder[type].map(callback),
};
return {
...query,
builder: updatedBuilder,
};
}),
} as any);
// Default mock for useGetSearchQueryParam
jest.mocked(useGetSearchQueryParam).mockReturnValue(null);
});
it('should render without crashing', () => {
@@ -113,7 +154,7 @@ describe('TableViewActions', () => {
isfilterInLoading={defaultProps.isfilterInLoading}
isfilterOutLoading={defaultProps.isfilterOutLoading}
onClickHandler={defaultProps.onClickHandler}
onGroupByAttribute={defaultProps.onGroupByAttribute}
handleChangeSelectedView={defaultProps.handleChangeSelectedView}
/>,
);
expect(screen.getByText(TEST_VALUE)).toBeInTheDocument();
@@ -135,7 +176,7 @@ describe('TableViewActions', () => {
isfilterInLoading={defaultProps.isfilterInLoading}
isfilterOutLoading={defaultProps.isfilterOutLoading}
onClickHandler={defaultProps.onClickHandler}
onGroupByAttribute={defaultProps.onGroupByAttribute}
handleChangeSelectedView={defaultProps.handleChangeSelectedView}
/>,
);
// Verify that action buttons are not rendered for restricted fields
@@ -154,13 +195,100 @@ describe('TableViewActions', () => {
isfilterInLoading={defaultProps.isfilterInLoading}
isfilterOutLoading={defaultProps.isfilterOutLoading}
onClickHandler={defaultProps.onClickHandler}
onGroupByAttribute={defaultProps.onGroupByAttribute}
handleChangeSelectedView={defaultProps.handleChangeSelectedView}
/>,
);
// Verify that action buttons are rendered for non-restricted fields
expect(container.querySelector(ACTION_BUTTON_TEST_ID)).toBeInTheDocument();
});
it('should call handleChangeSelectedView when clicking group by', () => {
const mockStagedQuery = {
id: 'test-query-id',
queryType: 'queryBuilder',
builder: {
queryData: [
{
queryName: 'A',
dataSource: 'logs',
aggregateOperator: 'count',
functions: [],
filter: {},
groupBy: [],
expression: '',
disabled: false,
having: [],
limit: null,
stepInterval: null,
orderBy: [],
legend: '',
},
],
queryFormulas: [],
queryTraceOperator: [],
},
promql: [],
clickhouse_sql: [],
};
const mockUpdateQueriesData = jest.fn((query, type, callback) => {
const section = query.builder?.[type];
if (!Array.isArray(section)) {
return query;
}
return {
...query,
builder: {
...query.builder,
[type]: section.map(callback),
},
};
});
jest.mocked(useQueryBuilder).mockReturnValue({
stagedQuery: mockStagedQuery,
updateQueriesData: mockUpdateQueriesData,
} as any);
jest.mocked(useGetSearchQueryParam).mockReturnValue(null);
render(
<TableViewActions
fieldData={defaultProps.fieldData}
record={defaultProps.record}
isListViewPanel={defaultProps.isListViewPanel}
isfilterInLoading={defaultProps.isfilterInLoading}
isfilterOutLoading={defaultProps.isfilterOutLoading}
onClickHandler={defaultProps.onClickHandler}
handleChangeSelectedView={defaultProps.handleChangeSelectedView}
/>,
);
fireEvent.click(screen.getByText('Group By Attribute'));
expect(defaultProps.handleChangeSelectedView).toHaveBeenCalledWith(
ExplorerViews.TIMESERIES,
expect.objectContaining({
name: '',
id: 'test-query-id',
query: expect.objectContaining({
builder: expect.objectContaining({
queryData: expect.arrayContaining([
expect.objectContaining({
groupBy: expect.arrayContaining([
expect.objectContaining({
key: TEST_FIELD,
type: '',
}),
]),
}),
]),
}),
}),
}),
);
});
it('should not render action buttons in list view panel', () => {
const { container } = render(
<TableViewActions
@@ -170,7 +298,7 @@ describe('TableViewActions', () => {
isfilterInLoading={defaultProps.isfilterInLoading}
isfilterOutLoading={defaultProps.isfilterOutLoading}
onClickHandler={defaultProps.onClickHandler}
onGroupByAttribute={defaultProps.onGroupByAttribute}
handleChangeSelectedView={defaultProps.handleChangeSelectedView}
/>,
);
// Verify that action buttons are not rendered in list view panel
@@ -200,7 +328,7 @@ describe('TableViewActions', () => {
isfilterInLoading: false,
isfilterOutLoading: false,
onClickHandler: jest.fn(),
onGroupByAttribute: jest.fn(),
handleChangeSelectedView: jest.fn(),
};
// Render component with body field
@@ -212,7 +340,7 @@ describe('TableViewActions', () => {
isfilterInLoading={bodyProps.isfilterInLoading}
isfilterOutLoading={bodyProps.isfilterOutLoading}
onClickHandler={bodyProps.onClickHandler}
onGroupByAttribute={bodyProps.onGroupByAttribute}
handleChangeSelectedView={bodyProps.handleChangeSelectedView}
/>,
);

View File

@@ -47,7 +47,6 @@ function ColumnView({
onSetActiveLog: handleSetActiveLog,
onClearActiveLog: handleClearActiveLog,
onAddToQuery: handleAddToQuery,
onGroupByAttribute: handleGroupByAttribute,
} = useActiveLog();
const [showActiveLog, setShowActiveLog] = useState<boolean>(false);
@@ -271,7 +270,6 @@ function ColumnView({
onClose={handleLogDetailClose}
onAddToQuery={handleAddToQuery}
onClickActionItem={handleAddToQuery}
onGroupByAttribute={handleGroupByAttribute}
/>
)}
</div>

View File

@@ -58,7 +58,7 @@ const CustomTableRow: TableComponents<ILog>['TableRow'] = ({
const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
function InfinityTableView(
{ isLoading, tableViewProps, infitiyTableProps },
{ isLoading, tableViewProps, infitiyTableProps, handleChangeSelectedView },
ref,
): JSX.Element | null {
const {
@@ -72,7 +72,6 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
onSetActiveLog,
onClearActiveLog,
onAddToQuery,
onGroupByAttribute,
} = useActiveLog();
const { dataSource, columns } = useTableView({
@@ -187,7 +186,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
onClose={handleClearActiveContextLog}
onAddToQuery={handleAddToQuery}
selectedTab={VIEW_TYPES.CONTEXT}
onGroupByAttribute={onGroupByAttribute}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
<LogDetail
@@ -196,7 +195,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
onGroupByAttribute={onGroupByAttribute}
handleChangeSelectedView={handleChangeSelectedView}
/>
</>
);

View File

@@ -1,4 +1,5 @@
import { UseTableViewProps } from 'components/Logs/TableView/types';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
export type InfinityTableProps = {
isLoading?: boolean;
@@ -6,4 +7,5 @@ export type InfinityTableProps = {
infitiyTableProps?: {
onEndReached: (index: number) => void;
};
handleChangeSelectedView?: ChangeViewFunctionType;
};

View File

@@ -1,3 +1,4 @@
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import APIError from 'types/api/error';
import { ILog } from 'types/api/logs/log';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
@@ -12,4 +13,5 @@ export type LogsExplorerListProps = {
error?: Error | APIError;
isFilterApplied: boolean;
isFrequencyChartVisible: boolean;
handleChangeSelectedView?: ChangeViewFunctionType;
};

View File

@@ -48,6 +48,7 @@ function LogsExplorerList({
isError,
error,
isFilterApplied,
handleChangeSelectedView,
}: LogsExplorerListProps): JSX.Element {
const ref = useRef<VirtuosoHandle>(null);
const { activeLogId } = useCopyLogLink();
@@ -56,7 +57,6 @@ function LogsExplorerList({
activeLog,
onClearActiveLog,
onAddToQuery,
onGroupByAttribute,
onSetActiveLog,
} = useActiveLog();
@@ -100,6 +100,7 @@ function LogsExplorerList({
linesPerRow={options.maxLines}
selectedFields={selectedFields}
fontSize={options.fontSize}
handleChangeSelectedView={handleChangeSelectedView}
/>
);
}
@@ -114,11 +115,13 @@ function LogsExplorerList({
activeLog={activeLog}
fontSize={options.fontSize}
linesPerRow={options.maxLines}
handleChangeSelectedView={handleChangeSelectedView}
/>
);
},
[
activeLog,
handleChangeSelectedView,
onAddToQuery,
onSetActiveLog,
options.fontSize,
@@ -149,10 +152,10 @@ function LogsExplorerList({
activeLogIndex,
}}
infitiyTableProps={{ onEndReached }}
handleChangeSelectedView={handleChangeSelectedView}
/>
);
}
function getMarginTop(): string {
switch (options.fontSize) {
case FontSize.SMALL:
@@ -195,6 +198,7 @@ function LogsExplorerList({
onEndReached,
getItemContent,
selectedFields,
handleChangeSelectedView,
]);
const isTraceToLogsNavigation = useMemo(() => {
@@ -273,8 +277,8 @@ function LogsExplorerList({
log={activeLog}
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onGroupByAttribute={onGroupByAttribute}
onClickActionItem={onAddToQuery}
handleChangeSelectedView={handleChangeSelectedView}
/>
</>
)}

View File

@@ -447,8 +447,9 @@ function LogsExplorerViewsContainer({
)}
<div className="logs-explorer-views-type-content">
{showLiveLogs && <LiveLogs />}
{showLiveLogs && (
<LiveLogs handleChangeSelectedView={handleChangeSelectedView} />
)}
{selectedPanelType === PANEL_TYPES.LIST && !showLiveLogs && (
<LogsExplorerList
isLoading={isLoading}
@@ -460,9 +461,9 @@ function LogsExplorerViewsContainer({
isError={isError}
error={error as APIError}
isFilterApplied={!isEmpty(listQuery?.filters?.items)}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
{selectedPanelType === PANEL_TYPES.TIME_SERIES && !showLiveLogs && (
<div className="time-series-view-container">
<div className="time-series-view-container-header">
@@ -483,7 +484,6 @@ function LogsExplorerViewsContainer({
/>
</div>
)}
{selectedPanelType === PANEL_TYPES.TABLE && !showLiveLogs && (
<LogsExplorerTable
data={

View File

@@ -89,7 +89,6 @@ function LogsPanelComponent({
onSetActiveLog,
onClearActiveLog,
onAddToQuery,
onGroupByAttribute,
} = useActiveLog();
const handleRow = useCallback(
@@ -171,7 +170,6 @@ function LogsPanelComponent({
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
onGroupByAttribute={onGroupByAttribute}
isListViewPanel
listViewPanelSelectedFields={widget?.selectedLogFields}
/>

View File

@@ -38,7 +38,6 @@ function LogsTable(props: LogsTableProps): JSX.Element {
activeLog,
onClearActiveLog,
onAddToQuery,
onGroupByAttribute,
onSetActiveLog,
} = useActiveLog();
@@ -157,7 +156,6 @@ function LogsTable(props: LogsTableProps): JSX.Element {
selectedTab={VIEW_TYPES.OVERVIEW}
log={activeLog}
onClose={onClearActiveLog}
onGroupByAttribute={onGroupByAttribute}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
/>

View File

@@ -14,7 +14,6 @@ function LogsList({ logs }: LogsListProps): JSX.Element {
onSetActiveLog,
onClearActiveLog,
onAddToQuery,
onGroupByAttribute,
} = useActiveLog();
const makeLogDetailsHandler = (log: ILog) => (): void => onSetActiveLog(log);
@@ -49,7 +48,6 @@ function LogsList({ logs }: LogsListProps): JSX.Element {
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
onGroupByAttribute={onGroupByAttribute}
/>
</div>
);

View File

@@ -1,4 +1,5 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { liveLogsCompositeQuery } from 'container/LiveLogs/constants';
import LiveLogsContainer from 'container/LiveLogs/LiveLogsContainer';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
@@ -6,7 +7,11 @@ import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { useEffect } from 'react';
import { DataSource } from 'types/common/queryBuilder';
function LiveLogs(): JSX.Element {
interface LiveLogsProps {
handleChangeSelectedView?: ChangeViewFunctionType;
}
function LiveLogs({ handleChangeSelectedView }: LiveLogsProps): JSX.Element {
useShareBuilderUrl({ defaultValue: liveLogsCompositeQuery });
const { handleSetConfig } = useQueryBuilder();
@@ -14,7 +19,13 @@ function LiveLogs(): JSX.Element {
handleSetConfig(PANEL_TYPES.LIST, DataSource.LOGS);
}, [handleSetConfig]);
return <LiveLogsContainer />;
return (
<LiveLogsContainer handleChangeSelectedView={handleChangeSelectedView} />
);
}
LiveLogs.defaultProps = {
handleChangeSelectedView: undefined,
};
export default LiveLogs;

View File

@@ -79,21 +79,19 @@ function LogsExplorer(): JSX.Element {
const handleChangeSelectedView = useCallback(
(view: ExplorerViews, querySearchParameters?: ICurrentQueryData): void => {
handleSetConfig(
defaultTo(explorerViewToPanelType[view], PANEL_TYPES.LIST),
DataSource.LOGS,
const nextPanelType = defaultTo(
explorerViewToPanelType[view],
PANEL_TYPES.LIST,
);
handleSetConfig(nextPanelType, DataSource.LOGS);
setSelectedView(view);
if (view !== ExplorerViews.LIST) {
setShowLiveLogs(false);
}
handleExplorerTabChange(
explorerViewToPanelType[view],
querySearchParameters,
);
handleExplorerTabChange(nextPanelType, querySearchParameters);
},
[handleSetConfig, handleExplorerTabChange, setSelectedView],
);

View File

@@ -26,18 +26,6 @@ body {
box-sizing: border-box;
}
// Theme transition animations
* {
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease,
box-shadow 0.3s ease;
}
// For components that shouldn't transition (like loading spinners, animations)
.no-transition,
.no-transition * {
transition: none !important;
}
// Respect user's reduced motion preference
@media (prefers-reduced-motion: reduce) {
* {
@@ -178,7 +166,7 @@ body {
height: 11px;
flex-shrink: 0;
cursor: pointer;
transition: all 0.2s ease;
transition: transform 0.2s ease;
position: relative;
&:hover {
@@ -292,7 +280,7 @@ body {
height: 11px;
flex-shrink: 0;
cursor: pointer;
transition: all 0.2s ease;
transition: transform 0.2s ease;
position: relative;
&:hover {

View File

@@ -344,6 +344,8 @@ const customRender = (
});
};
// eslint-disable-next-line import/export -- re-exporting custom render alongside @testing-library/react
export * from '@testing-library/react';
export { default as userEvent } from '@testing-library/user-event';
// eslint-disable-next-line import/export -- custom render wraps the original
export { customRender as render };