Yunus M ab4f6adb19
Logs explorer design update (#4352)
* feat: logs explorer - new design

* feat: update styles

* feat: added new toolbar for logs explorer (#4336)

* feat: logs list view changes (#4348)

* feat: logs list view changes

* fix: list view and toolbar styles

* feat: side btns

* feat: added auto refresh handler

* feat: handle popover close for btn click date time

* feat: extract the common log actions btn component

* feat: update the button for log line actions

* fix: event propagation from context button

* feat: use styles from ui-library

* Query builder design update (#4359)

* feat: QB design update

* fix: add functionality and light mode styles

* fix: ts issues

* fix: update all css color variables to correct names

* fix: lint errors

* feat: new table view for logs explorer list section  (#4353)

* feat: table view changes for logs list

* feat: code refactor to support log line actions

* feat: code refactor to support log line actions

* fix: the positioning of the btns

* feat: fix the table onclick

* fix: header issue

* fix: on hover

* fix: type issue

* fix: eslint error

* fix: type errors (#4360)

* feat: handle light theme for logs explorer design changes (#4363)

* feat: handle light theme for list tables and dateTime selection

* feat: handle light theme for popover

* fix: address review comments

* feat: date time custom time modal to render inside the new popover (#4366)

* feat: single calender for range picker

* fix: edgecases

* feat: integrate date time selector across app

* fix: remove dangling border after element removal

* feat: handle qb design changes across the application

* feat: handle light theme

* feat: handle light theme

* fix: virtuoso scroll refresh issue

* feat: handle new typing changes for date time picker v2 (#4386)

Co-authored-by: Yunus M <myounis.ar@live.com>

* chore: styles improvement across new design (#4389)

* fix: improve date time styles

* feat: table view changes according to new design

* fix: button visibility in clickhouse and promQL headers (#4390)

* feat: change the tabs to new design buttons for query builder

* Settings theme change (#4368)

* feat: settings theme change

* [Refactor]: New design for Log details page (#4362)

New design for Log details page 

Co-authored-by: Vikrant Gupta <vikrant.thomso@gmail.com>
Co-authored-by: Yunus M <myounis.ar@live.com>

* feat: save view for new design (#4392)

* feat: save view for new design

* refactor: done with save view

* feat: update styles for logs detail view (#4407)

* feat: update styles for logs detail view

* feat: update styles for logs detail view

* feat: add raw view attributes in the logs list view (#4422)

* feat: add raw view attributes in the logs list view

* feat: add raw view attributes in the logs list view

* fix: raw attributes

* fix: logs UI improvements (#4426)

* fix: remove fixed times from the date time picker v2

* fix: added old logs explorer CTA in new designs

* feat: handle active logs indicator update

* fix: address review comments

* fix: old logs explorer page

* fix: remove info text and add relative time buttons

* fix: update logs explorer tab designs

* fix: update logs explorer tab designs

* fix: update logs explorer tab designs

* refactor: New design for Save views. (#4435)

* feat: [GH-4436]: date range enhancements (#4448)

* feat: [GH-4436]: when selecting custom time range it should be from start of day to end of date

* fix: custom time width and refresh text visibility issues (#4428)

---------

Co-authored-by: Yunus M <myounis.ar@live.com>

* feat: update ui (#4449)

* feat: added loading and error states for logs design (#4452)

* feat: added loading and error states for logs design

* feat: added error states for table view and time series view

* feat: handle error and loading states

* feat: loading states

* [Refactor]: Tab Switch deplay issue and UI improvement for Clickhouse (#4409)

* fix: switching between logs display tabs (#4457)

* [Feat]: View in Traces (#4450)

* refactor: datetime selector beside run query removed add to dashboard

* refactor: added tab for traces view details page

* refactor: done with the save view in traces

* fix: the gittery effect when navigatigating from views

* refactor: view tab view title light mode support

* refactor: removed console

* fix: gittery effect when switch view from views tabs

* refactor: separate traces routes

* refactor: remove query params

* chore: fix tsc issues

* fix: jest config issues

* fix: update TODO and remove extra braces

* feat: handle loading states and incorporate ui feedback (#4479)

* UI feedback updates (#4482)

* feat: handle loading and fix ui issues

* feat: ui updates

* fix: logs explorer issues (#4483)

* fix: logs explorer issues

* fix: jest test cases

* feat: support custom times unique to pages new design changes (#4485)

* fix: loading states for list log view (#4486)

* fix: logs search view query fix, logs details view - attribute tags alignment fix (#4489)

* fix: delete empty file

* fix: chart loading when scrolling logs (#4495)

* fix: chart should not load when scrolling the logs as it is already fetched

* fix: make the search bar as default rather than advanced options

* fix: rename show context to show in context

* fix: query range api not triggering on default select first load (#4498)

* Refactor: Log Explorer UI changes.  (#4502)

* refactor: used selected view enum

* refactor: updated hight of switch old button and tab border

* refactor: import fixes

* refactor: query builder border and button groups

* refactor: removed hypen from refreshed

* refactor: show delete button only when there is more than one query

* refactor: sqaure up the query build button groups

* refactor: updated css

* fix: additional filter color button shadow

* refactor: removed commented code and used selected panel enum

* refactor: updated typecheck script

* refactor: used enum selected view (#4504)

* fix: retain the current query on date time change (#4510)

* feat: added new icon for promQL and added tooltips for dashboards and alerts (#4512)

* feat: added new icon for promQL and added tooltips for dashboards and alerts

* fix: styles at 1440 px zoom

* fix: rename clickhouse to clickHouse

---------

Co-authored-by: Vikrant Gupta <54737045+Vikrant2520@users.noreply.github.com>
Co-authored-by: Vikrant Gupta <vikrant.thomso@gmail.com>
Co-authored-by: Rajat Dabade <rajat@signoz.io>
2024-02-12 00:23:19 +05:30

638 lines
16 KiB
TypeScript

/* eslint-disable sonarjs/cognitive-complexity */
import './LogsExplorerViews.styles.scss';
import { Button } from 'antd';
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
import { LOCALSTORAGE } from 'constants/localStorage';
import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes';
import { QueryParams } from 'constants/query';
import {
initialFilters,
initialQueriesMap,
initialQueryBuilderFormValues,
PANEL_TYPES,
} from 'constants/queryBuilder';
import { DEFAULT_PER_PAGE_VALUE } from 'container/Controls/config';
import ExplorerOptions from 'container/ExplorerOptions/ExplorerOptions';
import GoToTop from 'container/GoToTop';
import LogsExplorerChart from 'container/LogsExplorerChart';
import LogsExplorerList from 'container/LogsExplorerList';
import LogsExplorerTable from 'container/LogsExplorerTable';
import { useOptionsMenu } from 'container/OptionsMenu';
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils';
import { LogTimeRange } from 'hooks/logs/types';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useAxiosError from 'hooks/useAxiosError';
import useClickOutside from 'hooks/useClickOutside';
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData';
import { defaultTo, isEmpty } from 'lodash-es';
import { Sliders } from 'lucide-react';
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { Dashboard } from 'types/api/dashboard/getAll';
import { ILog } from 'types/api/logs/log';
import {
IBuilderQuery,
OrderByPayload,
Query,
TagFilter,
} from 'types/api/queryBuilder/queryBuilderData';
import {
DataSource,
LogsAggregatorOperator,
StringOperators,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
import { v4 } from 'uuid';
function LogsExplorerViews({
selectedView,
showHistogram,
}: {
selectedView: SELECTED_VIEWS;
showHistogram: boolean;
}): JSX.Element {
const { notifications } = useNotifications();
const history = useHistory();
// this is to respect the panel type present in the URL rather than defaulting it to list always.
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
const { activeLogId, timeRange, onTimeRangeChange } = useCopyLogLink();
const { queryData: pageSize } = useUrlQueryData(
QueryParams.pageSize,
DEFAULT_PER_PAGE_VALUE,
);
const { minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const currentMinTimeRef = useRef<number>(minTime);
// Context
const {
initialDataSource,
currentQuery,
stagedQuery,
panelType,
updateAllQueriesOperators,
handleSetConfig,
} = useQueryBuilder();
const [selectedPanelType, setSelectedPanelType] = useState<PANEL_TYPES>(
panelType || PANEL_TYPES.LIST,
);
const { handleExplorerTabChange } = useHandleExplorerTabChange();
// State
const [page, setPage] = useState<number>(1);
const [logs, setLogs] = useState<ILog[]>([]);
const [requestData, setRequestData] = useState<Query | null>(null);
const [showFormatMenuItems, setShowFormatMenuItems] = useState(false);
const handleAxisError = useAxiosError();
const listQuery = useMemo(() => {
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) return null;
return stagedQuery.builder.queryData.find((item) => !item.disabled) || null;
}, [stagedQuery]);
const orderByTimestamp: OrderByPayload | null = useMemo(() => {
const timestampOrderBy = listQuery?.orderBy.find(
(item) => item.columnName === 'timestamp',
);
return timestampOrderBy || null;
}, [listQuery]);
const isMultipleQueries = useMemo(
() =>
currentQuery.builder.queryData.length > 1 ||
currentQuery.builder.queryFormulas.length > 0,
[currentQuery],
);
const isGroupByExist = useMemo(() => {
const groupByCount: number = currentQuery.builder.queryData.reduce<number>(
(acc, query) => acc + query.groupBy.length,
0,
);
return groupByCount > 0;
}, [currentQuery]);
const isLimit: boolean = useMemo(() => {
if (!listQuery) return false;
if (!listQuery.limit) return false;
return logs.length >= listQuery.limit;
}, [logs.length, listQuery]);
const listChartQuery = useMemo(() => {
if (!stagedQuery || !listQuery) return null;
const modifiedQueryData: IBuilderQuery = {
...listQuery,
aggregateOperator: LogsAggregatorOperator.COUNT,
};
const modifiedQuery: Query = {
...stagedQuery,
builder: {
...stagedQuery.builder,
queryData: stagedQuery.builder.queryData.map((item) => ({
...item,
...modifiedQueryData,
})),
},
};
return modifiedQuery;
}, [stagedQuery, listQuery]);
const exportDefaultQuery = useMemo(
() =>
updateAllQueriesOperators(
currentQuery || initialQueriesMap.logs,
PANEL_TYPES.TIME_SERIES,
DataSource.LOGS,
),
[currentQuery, updateAllQueriesOperators],
);
const handleModeChange = (panelType: PANEL_TYPES): void => {
if (selectedView === SELECTED_VIEWS.SEARCH) {
handleSetConfig(panelType, DataSource.LOGS);
}
setShowFormatMenuItems(false);
handleExplorerTabChange(panelType);
};
const {
data: listChartData,
isFetching: isFetchingListChartData,
isLoading: isLoadingListChartData,
} = useGetExplorerQueryRange(listChartQuery, PANEL_TYPES.TIME_SERIES, {
enabled: !!listChartQuery && panelType === PANEL_TYPES.LIST,
});
const { data, isLoading, isFetching, isError } = useGetExplorerQueryRange(
requestData,
panelType,
{
keepPreviousData: true,
enabled: !isLimit && !!requestData,
},
{
...(timeRange &&
activeLogId &&
!logs.length && {
start: timeRange.start,
end: timeRange.end,
}),
},
);
const getRequestData = useCallback(
(
query: Query | null,
params: {
page: number;
log: ILog | null;
pageSize: number;
filters: TagFilter;
},
): Query | null => {
if (!query) return null;
const paginateData = getPaginationQueryData({
filters: params.filters,
listItemId: params.log ? params.log.id : null,
orderByTimestamp,
page: params.page,
pageSize: params.pageSize,
});
const queryData: IBuilderQuery[] =
query.builder.queryData.length > 1
? query.builder.queryData
: [
{
...(listQuery || initialQueryBuilderFormValues),
...paginateData,
},
];
const data: Query = {
...query,
builder: {
...query.builder,
queryData,
},
};
return data;
},
[orderByTimestamp, listQuery],
);
const handleEndReached = useCallback(
(index: number) => {
if (!listQuery) return;
if (isLimit) return;
if (logs.length < pageSize) return;
const { limit, filters } = listQuery;
const lastLog = logs[index];
const nextLogsLength = logs.length + pageSize;
const nextPageSize =
limit && nextLogsLength >= limit ? limit - logs.length : pageSize;
if (!stagedQuery) return;
const newRequestData = getRequestData(stagedQuery, {
filters,
page: page + 1,
log: orderByTimestamp ? lastLog : null,
pageSize: nextPageSize,
});
setPage((prevPage) => prevPage + 1);
setRequestData(newRequestData);
},
[
isLimit,
logs,
listQuery,
pageSize,
stagedQuery,
getRequestData,
page,
orderByTimestamp,
],
);
const {
mutate: updateDashboard,
isLoading: isUpdateDashboardLoading,
} = useUpdateDashboard();
const handleExport = useCallback(
(dashboard: Dashboard | null): void => {
if (!dashboard || !panelType) return;
const panelTypeParam = AVAILABLE_EXPORT_PANEL_TYPES.includes(panelType)
? panelType
: PANEL_TYPES.TIME_SERIES;
const widgetId = v4();
const updatedDashboard = addEmptyWidgetInDashboardJSONWithQuery(
dashboard,
exportDefaultQuery,
widgetId,
panelTypeParam,
);
updateDashboard(updatedDashboard, {
onSuccess: (data) => {
if (data.error) {
const message =
data.error === 'feature usage exceeded' ? (
<span>
Panel limit exceeded for {DataSource.LOGS} in community edition. Please
checkout our paid plans{' '}
<a
href="https://signoz.io/pricing/?utm_source=product&utm_medium=dashboard-limit"
rel="noreferrer noopener"
target="_blank"
>
here
</a>
</span>
) : (
data.error
);
notifications.error({
message,
});
return;
}
const dashboardEditView = generateExportToDashboardLink({
query: exportDefaultQuery,
panelType: panelTypeParam,
dashboardId: data.payload?.uuid || '',
widgetId,
});
history.push(dashboardEditView);
},
onError: handleAxisError,
});
},
[
exportDefaultQuery,
history,
notifications,
panelType,
updateDashboard,
handleAxisError,
],
);
useEffect(() => {
const shouldChangeView =
(isMultipleQueries || isGroupByExist) &&
selectedView !== SELECTED_VIEWS.SEARCH;
if (selectedPanelType === PANEL_TYPES.LIST && shouldChangeView) {
handleExplorerTabChange(PANEL_TYPES.TIME_SERIES);
setSelectedPanelType(PANEL_TYPES.TIME_SERIES);
}
if (panelType) {
setSelectedPanelType(panelType);
}
}, [
isMultipleQueries,
isGroupByExist,
selectedPanelType,
selectedView,
handleExplorerTabChange,
panelType,
]);
useEffect(() => {
if (
selectedView &&
selectedView === SELECTED_VIEWS.SEARCH &&
handleSetConfig
) {
handleSetConfig(defaultTo(panelTypes, PANEL_TYPES.LIST), DataSource.LOGS);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
const currentParams = data?.params as Omit<LogTimeRange, 'pageSize'>;
const currentData = data?.payload.data.newResult.data.result || [];
if (currentData.length > 0 && currentData[0].list) {
const currentLogs: ILog[] = currentData[0].list.map((item) => ({
...item.data,
timestamp: item.timestamp,
}));
const newLogs = [...logs, ...currentLogs];
setLogs(newLogs);
onTimeRangeChange({
start: currentParams?.start,
end: timeRange?.end || currentParams?.end,
pageSize: newLogs.length,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);
useEffect(() => {
if (
requestData?.id !== stagedQuery?.id ||
currentMinTimeRef.current !== minTime
) {
const newRequestData = getRequestData(stagedQuery, {
filters: listQuery?.filters || initialFilters,
page: 1,
log: null,
pageSize:
timeRange?.pageSize && activeLogId ? timeRange?.pageSize : pageSize,
});
setLogs([]);
setPage(1);
setRequestData(newRequestData);
currentMinTimeRef.current = minTime;
}
}, [
stagedQuery,
requestData,
getRequestData,
listQuery,
pageSize,
minTime,
timeRange,
activeLogId,
onTimeRangeChange,
panelType,
selectedView,
]);
const { options, config } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
dataSource: initialDataSource || DataSource.METRICS,
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
});
const chartData = useMemo(() => {
if (!stagedQuery) return [];
if (panelType === PANEL_TYPES.LIST) {
if (listChartData && listChartData.payload.data.result.length > 0) {
return listChartData.payload.data.result;
}
return [];
}
if (!data || data.payload.data.result.length === 0) return [];
const isGroupByExist = stagedQuery.builder.queryData.some(
(queryData) => queryData.groupBy.length > 0,
);
const firstPayloadQuery = data.payload.data.result.find(
(item) => item.queryName === listQuery?.queryName,
);
const firstPayloadQueryArray = firstPayloadQuery ? [firstPayloadQuery] : [];
return isGroupByExist ? data.payload.data.result : firstPayloadQueryArray;
}, [stagedQuery, panelType, data, listChartData, listQuery]);
const formatItems = [
{
key: 'raw',
label: 'Raw',
data: {
title: 'max lines per row',
},
},
{
key: 'list',
label: 'Default',
},
{
key: 'table',
label: 'Column',
data: {
title: 'columns',
},
},
];
const handleToggleShowFormatOptions = (): void =>
setShowFormatMenuItems(!showFormatMenuItems);
const menuRef = useRef<HTMLDivElement>(null);
useClickOutside({
ref: menuRef,
onClickOutside: () => {
if (showFormatMenuItems) {
setShowFormatMenuItems(false);
}
},
});
return (
<div className="logs-explorer-views-container">
{showHistogram && (
<LogsExplorerChart
className="logs-histogram"
isLoading={isFetchingListChartData || isLoadingListChartData}
data={chartData}
/>
)}
<div className="logs-explorer-views-types">
<div className="views-tabs-container">
<Button.Group className="views-tabs">
<Button
value={PANEL_TYPES.LIST}
className={
// eslint-disable-next-line sonarjs/no-duplicate-string
selectedPanelType === PANEL_TYPES.LIST ? 'selected_view tab' : 'tab'
}
disabled={
(isMultipleQueries || isGroupByExist) && selectedView !== 'search'
}
onClick={(): void => handleModeChange(PANEL_TYPES.LIST)}
>
List view
</Button>
<Button
value={PANEL_TYPES.TIME_SERIES}
className={
// eslint-disable-next-line sonarjs/no-duplicate-string
selectedPanelType === PANEL_TYPES.TIME_SERIES
? 'selected_view tab'
: 'tab'
}
onClick={(): void => handleModeChange(PANEL_TYPES.TIME_SERIES)}
>
Time series
</Button>
<Button
value={PANEL_TYPES.TABLE}
className={
// eslint-disable-next-line sonarjs/no-duplicate-string
selectedPanelType === PANEL_TYPES.TABLE ? 'selected_view tab' : 'tab'
}
onClick={(): void => handleModeChange(PANEL_TYPES.TABLE)}
>
Table
</Button>
</Button.Group>
<div className="logs-actions-container">
{selectedPanelType === PANEL_TYPES.LIST && (
<div className="tab-options">
<div className="format-options-container" ref={menuRef}>
<Button
className="periscope-btn"
onClick={handleToggleShowFormatOptions}
icon={<Sliders size={14} />}
/>
{showFormatMenuItems && (
<LogsFormatOptionsMenu
title="FORMAT"
items={formatItems}
selectedOptionFormat={options.format}
config={config}
/>
)}
</div>
</div>
)}
</div>
</div>
<div className="logs-explorer-views-type-content">
{selectedPanelType === PANEL_TYPES.LIST && (
<LogsExplorerList
isLoading={isLoading}
isFetching={isFetching}
currentStagedQueryData={listQuery}
logs={logs}
onEndReached={handleEndReached}
isError={isError}
isFilterApplied={!isEmpty(listQuery?.filters.items)}
/>
)}
{selectedPanelType === PANEL_TYPES.TIME_SERIES && (
<TimeSeriesView
isLoading={isLoading || isFetching}
data={data}
isError={isError}
isFilterApplied={!isEmpty(listQuery?.filters.items)}
/>
)}
{selectedPanelType === PANEL_TYPES.TABLE && (
<LogsExplorerTable
data={data?.payload.data.newResult.data.result || []}
isLoading={isLoading || isFetching}
isError={isError}
/>
)}
</div>
</div>
<GoToTop />
<ExplorerOptions
disabled={!stagedQuery}
query={exportDefaultQuery}
isLoading={isUpdateDashboardLoading}
onExport={handleExport}
sourcepage={DataSource.LOGS}
/>
</div>
);
}
export default memo(LogsExplorerViews);