mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-30 12:30:59 +00:00
Compare commits
1 Commits
improveTra
...
feat/sessi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63b9331c78 |
@@ -26,7 +26,7 @@ const config: Config.InitialOptions = {
|
||||
'^.+\\.(js|jsx)$': 'babel-jest',
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|@signozhq/table|@signozhq/calendar|@signozhq/input|@signozhq/popover|@signozhq/button|@signozhq/sonner|@signozhq/*|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn)/)',
|
||||
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|@signozhq/calendar|@signozhq/input|@signozhq/popover|@signozhq/button|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn)/)',
|
||||
],
|
||||
setupFilesAfterEnv: ['<rootDir>jest.setup.ts'],
|
||||
testPathIgnorePatterns: ['/node_modules/', '/public/'],
|
||||
|
||||
@@ -48,8 +48,6 @@
|
||||
"@signozhq/design-tokens": "1.1.4",
|
||||
"@signozhq/input": "0.0.2",
|
||||
"@signozhq/popover": "0.0.0",
|
||||
"@signozhq/sonner": "0.1.0",
|
||||
"@signozhq/table": "0.3.4",
|
||||
"@tanstack/react-table": "8.20.6",
|
||||
"@tanstack/react-virtual": "3.11.2",
|
||||
"@uiw/codemirror-theme-copilot": "4.23.11",
|
||||
@@ -135,6 +133,7 @@
|
||||
"redux": "^4.0.5",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"rehype-raw": "7.0.0",
|
||||
"rrweb-player": "1.0.0-alpha.4",
|
||||
"stream": "^0.0.2",
|
||||
"style-loader": "1.3.0",
|
||||
"styled-components": "^5.3.11",
|
||||
@@ -175,6 +174,7 @@
|
||||
"@commitlint/config-conventional": "^16.2.4",
|
||||
"@faker-js/faker": "9.3.0",
|
||||
"@jest/globals": "^27.5.1",
|
||||
"@rrweb/types": "2.0.0-alpha.18",
|
||||
"@testing-library/jest-dom": "5.16.5",
|
||||
"@testing-library/react": "13.4.0",
|
||||
"@testing-library/user-event": "14.4.3",
|
||||
|
||||
@@ -295,3 +295,15 @@ export const MetricsExplorer = Loadable(
|
||||
export const ApiMonitoring = Loadable(
|
||||
() => import(/* webpackChunkName: "ApiMonitoring" */ 'pages/ApiMonitoring'),
|
||||
);
|
||||
|
||||
export const SessionRecordings = Loadable(
|
||||
() =>
|
||||
import(/* webpackChunkName: "SessionRecordings" */ 'pages/SessionRecording'),
|
||||
);
|
||||
|
||||
export const SessionRecordingsDetail = Loadable(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "SessionRecordingsDetail" */ 'pages/SessionRecording/SessionDetail'
|
||||
),
|
||||
);
|
||||
|
||||
@@ -54,6 +54,8 @@ import {
|
||||
WorkspaceAccessRestricted,
|
||||
WorkspaceBlocked,
|
||||
WorkspaceSuspended,
|
||||
SessionRecordings,
|
||||
SessionRecordingsDetail,
|
||||
} from './pageComponents';
|
||||
|
||||
const routes: AppRoutes[] = [
|
||||
@@ -450,6 +452,20 @@ const routes: AppRoutes[] = [
|
||||
key: 'METER_EXPLORER',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.SESSION_RECORDINGS,
|
||||
exact: true,
|
||||
component: SessionRecordings,
|
||||
key: 'SESSION_RECORDINGS',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.SESSION_RECORDINGS_DETAIL,
|
||||
exact: true,
|
||||
component: SessionRecordingsDetail,
|
||||
key: 'SESSION_RECORDINGS_DETAIL',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.METER_EXPLORER_VIEWS,
|
||||
exact: true,
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
} from 'container/LogDetailedView/utils';
|
||||
import useInitialQuery from 'container/LogsExplorerContext/useInitialQuery';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
@@ -40,7 +39,7 @@ import {
|
||||
TextSelect,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useCopyToClipboard, useLocation } from 'react-use';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -95,8 +94,6 @@ function LogDetailInner({
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const { onLogCopy } = useCopyLogLink(log?.id);
|
||||
|
||||
const LogJsonData = log ? aggregateAttributesResourcesToString(log) : '';
|
||||
|
||||
const handleModeChange = (e: RadioChangeEvent): void => {
|
||||
@@ -149,34 +146,6 @@ function LogDetailInner({
|
||||
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`);
|
||||
};
|
||||
|
||||
const handleQueryExpressionChange = useCallback(
|
||||
(value: string, queryIndex: number) => {
|
||||
// update the query at the given index
|
||||
setContextQuery((prev) => {
|
||||
if (!prev) return prev;
|
||||
|
||||
return {
|
||||
...prev,
|
||||
builder: {
|
||||
...prev.builder,
|
||||
queryData: prev.builder.queryData.map((query, idx) =>
|
||||
idx === queryIndex
|
||||
? {
|
||||
...query,
|
||||
filter: {
|
||||
...query.filter,
|
||||
expression: value,
|
||||
},
|
||||
}
|
||||
: query,
|
||||
),
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleRunQuery = (expression: string): void => {
|
||||
let updatedContextQuery = cloneDeep(contextQuery);
|
||||
|
||||
@@ -336,19 +305,11 @@ function LogDetailInner({
|
||||
onClick={handleFilterVisible}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Tooltip title="Copy Log Link" placement="left" aria-label="Copy Log Link">
|
||||
<Button
|
||||
className="action-btn"
|
||||
icon={<Copy size={16} />}
|
||||
onClick={onLogCopy}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{isFilterVisible && contextQuery?.builder.queryData[0] && (
|
||||
<div className="log-detail-drawer-query-container">
|
||||
<QuerySearch
|
||||
onChange={(value): void => handleQueryExpressionChange(value, 0)}
|
||||
onChange={(): void => {}}
|
||||
dataSource={DataSource.LOGS}
|
||||
queryData={contextQuery?.builder.queryData[0]}
|
||||
onRun={handleRunQuery}
|
||||
|
||||
@@ -56,8 +56,6 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
.map(({ name }) => ({
|
||||
title: name,
|
||||
dataIndex: name,
|
||||
accessorKey: name,
|
||||
id: name.toLowerCase().replace(/\./g, '_'),
|
||||
key: name,
|
||||
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
|
||||
props: {
|
||||
@@ -85,10 +83,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
// We do not need any title and data index for the log state indicator
|
||||
title: '',
|
||||
dataIndex: '',
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
key: 'state-indicator',
|
||||
accessorKey: 'state-indicator',
|
||||
id: 'state-indicator',
|
||||
render: (_, item): ColumnTypeRender<Record<string, unknown>> => ({
|
||||
children: (
|
||||
<div className={cx('state-indicator', fontSize)}>
|
||||
@@ -106,8 +101,6 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
title: 'timestamp',
|
||||
dataIndex: 'timestamp',
|
||||
key: 'timestamp',
|
||||
accessorKey: 'timestamp',
|
||||
id: 'timestamp',
|
||||
// https://github.com/ant-design/ant-design/discussions/36886
|
||||
render: (
|
||||
field: string | number,
|
||||
@@ -142,8 +135,6 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
title: 'body',
|
||||
dataIndex: 'body',
|
||||
key: 'body',
|
||||
accessorKey: 'body',
|
||||
id: 'body',
|
||||
render: (
|
||||
field: string | number,
|
||||
): ColumnTypeRender<Record<string, unknown>> => ({
|
||||
|
||||
@@ -33,5 +33,4 @@ export enum LOCALSTORAGE {
|
||||
QUICK_FILTERS_SETTINGS_ANNOUNCEMENT = 'QUICK_FILTERS_SETTINGS_ANNOUNCEMENT',
|
||||
FUNNEL_STEPS = 'FUNNEL_STEPS',
|
||||
LAST_USED_CUSTOM_TIME_RANGES = 'LAST_USED_CUSTOM_TIME_RANGES',
|
||||
SHOW_FREQUENCY_CHART = 'SHOW_FREQUENCY_CHART',
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ const ROUTES = {
|
||||
GET_STARTED_AZURE_MONITORING: '/get-started/azure-monitoring',
|
||||
USAGE_EXPLORER: '/usage-explorer',
|
||||
APPLICATION: '/services',
|
||||
SESSION_RECORDINGS: '/session-recordings',
|
||||
SESSION_RECORDINGS_DETAIL: '/session-recordings/:sessionId',
|
||||
ALL_DASHBOARD: '/dashboard',
|
||||
DASHBOARD: '/dashboard/:dashboardId',
|
||||
DASHBOARD_WIDGET: '/dashboard/:dashboardId/:widgetId',
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import './AppLayout.styles.scss';
|
||||
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Toaster } from '@signozhq/sonner';
|
||||
import { Flex } from 'antd';
|
||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||
@@ -853,8 +852,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
{showChangelogModal && changelog && (
|
||||
<ChangelogModal changelog={changelog} onClose={toggleChangelogModal} />
|
||||
)}
|
||||
|
||||
<Toaster />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.explorer-options-container {
|
||||
position: fixed;
|
||||
bottom: 0px;
|
||||
bottom: 8px;
|
||||
left: calc(50% + 240px);
|
||||
transform: translate(calc(-50% - 120px), 0);
|
||||
transition: left 0.2s linear;
|
||||
@@ -74,7 +74,6 @@
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
z-index: 1;
|
||||
|
||||
.ant-select-selector {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
}
|
||||
|
||||
.explorer-show-btn {
|
||||
border-radius: 6px 6px 0px 0px;
|
||||
border-radius: 10px 10px 0px 0px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: rgba(22, 24, 29, 0.4);
|
||||
box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.25);
|
||||
|
||||
@@ -24,7 +24,7 @@ import { AnimatePresence } from 'motion/react';
|
||||
import * as motion from 'motion/react-client';
|
||||
import Card from 'periscope/components/Card/Card';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import { UserPreference } from 'types/api/preferences/preference';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
@@ -40,6 +40,9 @@ import HomeChecklist, { ChecklistItem } from './HomeChecklist/HomeChecklist';
|
||||
import SavedViews from './SavedViews/SavedViews';
|
||||
import Services from './Services/Services';
|
||||
import StepsProgress from './StepsProgress/StepsProgress';
|
||||
import events from './rows.json';
|
||||
import { eventWithTime } from '@rrweb/types';
|
||||
import RRWebPlayer from 'pages/SessionRecording/RRWebPlayer';
|
||||
|
||||
const homeInterval = 30 * 60 * 1000;
|
||||
|
||||
@@ -168,6 +171,37 @@ export default function Home(): JSX.Element {
|
||||
false,
|
||||
);
|
||||
|
||||
const playerContainerRef = useRef(null);
|
||||
// const [player, setPlayer] = useState<init | null>(null);
|
||||
const [parsedEvents, setParsedEvents] = useState<eventWithTime[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (events.rows.length > 0) {
|
||||
// Initialize the rrweb player with events
|
||||
const parsedEvents = events.rows.map((event) => {
|
||||
return JSON.parse(event.data.body);
|
||||
});
|
||||
|
||||
setParsedEvents(parsedEvents);
|
||||
// const replayPlayer = new init({
|
||||
// target: (playerContainerRef.current as unknown) as HTMLElement,
|
||||
// props: {
|
||||
// events: parsedEvents, // Pass the captured events to the player
|
||||
// speed: 1, // Normal speed (can be adjusted)
|
||||
// showDebug: false, // Optionally show debug info
|
||||
// },
|
||||
// });
|
||||
|
||||
// // Save the player instance for future use if needed
|
||||
// setPlayer(replayPlayer);
|
||||
|
||||
// // Cleanup on unmount
|
||||
// return () => {
|
||||
// replayPlayer.destroy();
|
||||
// };
|
||||
}
|
||||
}, [events]); // Re-run effect when events change
|
||||
|
||||
const processUserPreferences = (userPreferences: UserPreference[]): void => {
|
||||
const checklistSkipped = Boolean(
|
||||
userPreferences?.find(
|
||||
@@ -311,6 +345,9 @@ export default function Home(): JSX.Element {
|
||||
|
||||
return (
|
||||
<div className="home-container">
|
||||
<p>hello world</p>
|
||||
<RRWebPlayer events={parsedEvents} options={{ autoPlay: false }} />
|
||||
{/*
|
||||
<div className="sticky-header">
|
||||
{showBanner && (
|
||||
<div className="home-container-banner">
|
||||
@@ -378,7 +415,6 @@ export default function Home(): JSX.Element {
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="home-content">
|
||||
<div className="home-left-content">
|
||||
<DataSourceInfo
|
||||
@@ -764,7 +800,7 @@ export default function Home(): JSX.Element {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
3724
frontend/src/container/Home/rows.json
Normal file
3724
frontend/src/container/Home/rows.json
Normal file
File diff suppressed because one or more lines are too long
@@ -14,7 +14,4 @@ export const ContentWrapper = styled(Row)`
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
padding-bottom: 4rem;
|
||||
padding-top: 1rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Card } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const CardStyled = styled(Card)`
|
||||
border: none !important;
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
.ant-card-body {
|
||||
height: 200px;
|
||||
min-height: 200px;
|
||||
padding: 0 16px 16px 16px;
|
||||
font-family: 'Geist Mono';
|
||||
}
|
||||
`;
|
||||
@@ -1,25 +0,0 @@
|
||||
.logs-frequency-chart-container {
|
||||
height: 200px;
|
||||
min-height: 200px;
|
||||
border-bottom: 1px solid #262626;
|
||||
|
||||
.ant-card-body {
|
||||
height: 200px;
|
||||
min-height: 200px;
|
||||
padding: 0 16px 16px 16px;
|
||||
font-family: 'Geist Mono';
|
||||
}
|
||||
|
||||
.logs-frequency-chart-loading {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.logs-frequency-chart-container {
|
||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import './LogsExplorerChart.styles.scss';
|
||||
|
||||
import Graph from 'components/Graph';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { QueryParams } from 'constants/query';
|
||||
@@ -17,6 +15,7 @@ import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { LogsExplorerChartProps } from './LogsExplorerChart.interfaces';
|
||||
import { CardStyled } from './LogsExplorerChart.styled';
|
||||
import { getColorsForSeverityLabels } from './utils';
|
||||
|
||||
function LogsExplorerChart({
|
||||
@@ -101,11 +100,9 @@ function LogsExplorerChart({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`${className} logs-frequency-chart-container`}>
|
||||
<CardStyled className={className}>
|
||||
{isLoading ? (
|
||||
<div className="logs-frequency-chart-loading">
|
||||
<Spinner size="default" height="100%" />
|
||||
</div>
|
||||
<Spinner size="default" height="100%" />
|
||||
) : (
|
||||
<Graph
|
||||
name="logsExplorerChart"
|
||||
@@ -118,7 +115,7 @@ function LogsExplorerChart({
|
||||
maxTime={chartMaxTime}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</CardStyled>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,240 +0,0 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { ColumnDef, DataTable, Row } from '@signozhq/table';
|
||||
import LogDetail from 'components/LogDetail';
|
||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||
import LogStateIndicator from 'components/Logs/LogStateIndicator/LogStateIndicator';
|
||||
import { getLogIndicatorTypeForTable } from 'components/Logs/LogStateIndicator/utils';
|
||||
import { useTableView } from 'components/Logs/TableView/useTableView';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||
import useDragColumns from 'hooks/useDragColumns';
|
||||
import { getDraggedColumns } from 'hooks/useDragColumns/utils';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { isEmpty, isEqual } from 'lodash-es';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
interface ColumnViewProps {
|
||||
logs: ILog[];
|
||||
onLoadMore: () => void;
|
||||
selectedFields: any[];
|
||||
isLoading: boolean;
|
||||
isFetching: boolean;
|
||||
|
||||
isFrequencyChartVisible: boolean;
|
||||
options: {
|
||||
maxLinesPerRow: number;
|
||||
fontSize: FontSize;
|
||||
};
|
||||
}
|
||||
|
||||
function ColumnView({
|
||||
logs,
|
||||
onLoadMore,
|
||||
selectedFields,
|
||||
isLoading,
|
||||
isFetching,
|
||||
isFrequencyChartVisible,
|
||||
options,
|
||||
}: ColumnViewProps): JSX.Element {
|
||||
const {
|
||||
activeLog,
|
||||
onSetActiveLog: handleSetActiveLog,
|
||||
onClearActiveLog: handleClearActiveLog,
|
||||
onAddToQuery: handleAddToQuery,
|
||||
onGroupByAttribute: handleGroupByAttribute,
|
||||
} = useActiveLog();
|
||||
|
||||
const { queryData: activeLogId } = useUrlQueryData<string | null>(
|
||||
QueryParams.activeLogId,
|
||||
null,
|
||||
);
|
||||
|
||||
const scrollToIndexRef = useRef<
|
||||
| ((
|
||||
rowIndex: number,
|
||||
options?: { align?: 'start' | 'center' | 'end' },
|
||||
) => void)
|
||||
| undefined
|
||||
>();
|
||||
|
||||
useEffect(() => {
|
||||
if (activeLogId) {
|
||||
const log = logs.find(({ id }) => id === activeLogId);
|
||||
|
||||
if (log) {
|
||||
handleSetActiveLog(log);
|
||||
}
|
||||
}
|
||||
}, [activeLogId, logs, handleSetActiveLog]);
|
||||
|
||||
const tableViewProps = {
|
||||
logs,
|
||||
fields: selectedFields,
|
||||
linesPerRow: options.maxLinesPerRow as number,
|
||||
fontSize: options.fontSize as FontSize,
|
||||
appendTo: 'end' as const,
|
||||
activeLogIndex: 0,
|
||||
};
|
||||
|
||||
const { dataSource, columns } = useTableView({
|
||||
...tableViewProps,
|
||||
onClickExpand: handleSetActiveLog,
|
||||
onOpenLogsContext: handleClearActiveLog,
|
||||
});
|
||||
|
||||
const { draggedColumns, onColumnOrderChange } = useDragColumns<
|
||||
Record<string, unknown>
|
||||
>(LOCALSTORAGE.LOGS_LIST_COLUMNS);
|
||||
|
||||
const tableColumns = useMemo(
|
||||
() => getDraggedColumns<Record<string, unknown>>(columns, draggedColumns),
|
||||
[columns, draggedColumns],
|
||||
);
|
||||
|
||||
const scrollToLog = useCallback(
|
||||
(logId: string): void => {
|
||||
const logIndex = logs.findIndex((log) => log.id === logId);
|
||||
|
||||
if (logIndex !== -1 && scrollToIndexRef.current) {
|
||||
scrollToIndexRef.current(logIndex, { align: 'center' });
|
||||
}
|
||||
},
|
||||
[logs],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeLogId) {
|
||||
scrollToLog(activeLogId);
|
||||
}
|
||||
}, [activeLogId]);
|
||||
|
||||
const args = {
|
||||
columns,
|
||||
tableId: 'virtualized-infinite-reorder-resize',
|
||||
enableSorting: false,
|
||||
enableFiltering: false,
|
||||
enableGlobalFilter: false,
|
||||
enableColumnReordering: true,
|
||||
enableColumnResizing: true,
|
||||
enableColumnPinning: false,
|
||||
enableRowSelection: false,
|
||||
enablePagination: false,
|
||||
showHeaders: true,
|
||||
defaultColumnWidth: 180,
|
||||
minColumnWidth: 80,
|
||||
maxColumnWidth: 480,
|
||||
// Virtualization + Infinite Scroll
|
||||
enableVirtualization: true,
|
||||
estimateRowSize: 56,
|
||||
overscan: 50,
|
||||
rowHeight: 56,
|
||||
enableInfiniteScroll: true,
|
||||
enableScrollRestoration: false,
|
||||
fixedHeight: isFrequencyChartVisible ? 560 : 760,
|
||||
enableDynamicRowHeight: false,
|
||||
};
|
||||
|
||||
const selectedColumns = useMemo(
|
||||
() =>
|
||||
tableColumns.map((field) => ({
|
||||
id: field.key?.toString().toLowerCase().replace(/\./g, '_'), // IMP - Replace dots with underscores as reordering does not work well for accessorKey with dots
|
||||
// accessorKey: field.name,
|
||||
accessorFn: (row: Record<string, string>): string =>
|
||||
row[field.key as string] as string,
|
||||
header: field.title as string,
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
size: field.key === 'state-indicator' ? 4 : 180,
|
||||
minSize: field.key === 'state-indicator' ? 4 : 120,
|
||||
maxSize: field.key === 'state-indicator' ? 4 : 1080,
|
||||
pin: field.key === 'state-indicator' ? 'left' : 'none',
|
||||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
cell: ({
|
||||
row,
|
||||
getValue,
|
||||
}: {
|
||||
row: Row<Record<string, string>>;
|
||||
getValue: () => string | JSX.Element;
|
||||
}): string | JSX.Element => {
|
||||
if (field.key === 'state-indicator') {
|
||||
const type = getLogIndicatorTypeForTable(row.original);
|
||||
const fontSize = options.fontSize as FontSize;
|
||||
|
||||
return <LogStateIndicator type={type} fontSize={fontSize} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`table-cell-content ${
|
||||
row.original.id === activeLog?.id ? 'active-log' : ''
|
||||
}`}
|
||||
>
|
||||
{getValue()}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
})),
|
||||
[tableColumns, options.fontSize, activeLog?.id],
|
||||
);
|
||||
|
||||
const handleColumnOrderChange = (newColumns: ColumnDef<any>[]): void => {
|
||||
if (isEmpty(newColumns) || isEqual(newColumns, selectedColumns)) return;
|
||||
|
||||
const formattedColumns = newColumns.map((column) => ({
|
||||
id: column.id,
|
||||
header: column.header,
|
||||
size: column.size,
|
||||
minSize: column.minSize,
|
||||
maxSize: column.maxSize,
|
||||
key: column.id,
|
||||
title: column.header as string,
|
||||
dataIndex: column.id,
|
||||
}));
|
||||
|
||||
onColumnOrderChange(formattedColumns);
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Row<Record<string, unknown>>): void => {
|
||||
const currentLog = logs.find(({ id }) => id === row.original.id);
|
||||
|
||||
handleSetActiveLog(currentLog as ILog);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`logs-list-table-view-container ${
|
||||
options.fontSize as FontSize
|
||||
} max-lines-${options.maxLinesPerRow as number}`}
|
||||
data-max-lines-per-row={options.maxLinesPerRow}
|
||||
data-font-size={options.fontSize}
|
||||
>
|
||||
<DataTable
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...args}
|
||||
columns={selectedColumns as ColumnDef<Record<string, string>, unknown>[]}
|
||||
data={dataSource}
|
||||
hasMore
|
||||
onLoadMore={onLoadMore}
|
||||
loadingMore={isLoading || isFetching}
|
||||
onColumnOrderChange={handleColumnOrderChange}
|
||||
onRowClick={handleRowClick}
|
||||
scrollToIndexRef={scrollToIndexRef}
|
||||
/>
|
||||
|
||||
{activeLog && (
|
||||
<LogDetail
|
||||
selectedTab={VIEW_TYPES.OVERVIEW}
|
||||
log={activeLog}
|
||||
onClose={handleClearActiveLog}
|
||||
onAddToQuery={handleAddToQuery}
|
||||
onClickActionItem={handleAddToQuery}
|
||||
onGroupByAttribute={handleGroupByAttribute}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ColumnView;
|
||||
@@ -11,5 +11,4 @@ export type LogsExplorerListProps = {
|
||||
isError: boolean;
|
||||
error?: Error | APIError;
|
||||
isFilterApplied: boolean;
|
||||
isFrequencyChartVisible: boolean;
|
||||
};
|
||||
|
||||
@@ -9,362 +9,4 @@
|
||||
letter-spacing: -0.005em;
|
||||
text-align: left;
|
||||
min-height: 500px;
|
||||
|
||||
.logs-list-table-view-container {
|
||||
.data-table-container {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
table {
|
||||
thead {
|
||||
tr {
|
||||
th {
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-size: 12px;
|
||||
color: white !important;
|
||||
|
||||
.cursor-col-resize {
|
||||
width: 2px !important;
|
||||
cursor: col-resize !important;
|
||||
opacity: 0.5 !important;
|
||||
|
||||
&:hover {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
border-bottom: 1px solid var(--bg-slate-400) !important;
|
||||
}
|
||||
|
||||
border-bottom: 1px solid var(--bg-slate-400) !important;
|
||||
background-color: var(--bg-ink-400) !important;
|
||||
|
||||
tr {
|
||||
&:hover {
|
||||
background-color: var(--bg-ink-400) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-size: 12px;
|
||||
padding: 4px !important;
|
||||
|
||||
white-space: pre-wrap !important;
|
||||
overflow: hidden !important;
|
||||
text-overflow: ellipsis !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-slate-500) !important;
|
||||
}
|
||||
|
||||
border-bottom: 1px solid var(--bg-slate-400) !important;
|
||||
}
|
||||
|
||||
tr:has(.active-log) {
|
||||
background-color: rgba(
|
||||
78,
|
||||
116,
|
||||
248,
|
||||
0.5
|
||||
) !important; // same as bg-robin-500
|
||||
color: var(--text-vanilla-100) !important;
|
||||
}
|
||||
}
|
||||
|
||||
thead {
|
||||
z-index: 0 !important;
|
||||
}
|
||||
|
||||
.log-state-indicator {
|
||||
padding-left: 0 !important;
|
||||
|
||||
.line {
|
||||
margin: 0 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sticky-header-table-container {
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.4rem;
|
||||
height: 0.4rem;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--bg-slate-300);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--bg-slate-200);
|
||||
}
|
||||
}
|
||||
|
||||
&.small {
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
.table-cell-content {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.medium {
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
.table-cell-content {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.large {
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
.table-cell-content {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.max-lines-1 {
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
.table-cell-content {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-max-lines-per-row='1'] {
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
.table-cell-content {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-max-lines-per-row='2'] {
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
.table-cell-content {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-max-lines-per-row='3'] {
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
.table-cell-content {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-max-lines-per-row='4'] {
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
.table-cell-content {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-max-lines-per-row='5'] {
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 5;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-max-lines-per-row='6'] {
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
.table-cell-content {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 6;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-max-lines-per-row='7'] {
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
.table-cell-content {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 7;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-max-lines-per-row='8'] {
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
.table-cell-content {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 8;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-max-lines-per-row='9'] {
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
.table-cell-content {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 9;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-max-lines-per-row='10'] {
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
.table-cell-content {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 10;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.logs-list-view-container {
|
||||
.logs-list-table-view-container {
|
||||
table {
|
||||
thead {
|
||||
tr {
|
||||
th {
|
||||
color: var(--text-ink-500) !important;
|
||||
}
|
||||
|
||||
border-bottom: 1px solid var(--bg-vanilla-300) !important;
|
||||
}
|
||||
|
||||
border-bottom: 1px solid var(--bg-vanilla-300) !important;
|
||||
background-color: var(--bg-vanilla-100) !important;
|
||||
|
||||
tr {
|
||||
&:hover {
|
||||
background-color: var(--bg-vanilla-300) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
&:hover {
|
||||
background-color: var(--bg-vanilla-300) !important;
|
||||
}
|
||||
|
||||
border-bottom: 1px solid var(--bg-vanilla-300) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sticky-header-table-container {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--bg-vanilla-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import { ILog } from 'types/api/logs/log';
|
||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
|
||||
import NoLogs from '../NoLogs/NoLogs';
|
||||
import ColumnView from './ColumnView/ColumnView';
|
||||
import InfinityTableView from './InfinityTableView';
|
||||
import { LogsExplorerListProps } from './LogsExplorerList.interfaces';
|
||||
import { InfinityWrapperStyled } from './styles';
|
||||
import {
|
||||
@@ -48,7 +48,6 @@ function LogsExplorerList({
|
||||
isError,
|
||||
error,
|
||||
isFilterApplied,
|
||||
isFrequencyChartVisible,
|
||||
}: LogsExplorerListProps): JSX.Element {
|
||||
const ref = useRef<VirtuosoHandle>(null);
|
||||
|
||||
@@ -91,7 +90,6 @@ function LogsExplorerList({
|
||||
});
|
||||
}
|
||||
}, [isLoading, isFetching, isError, logs.length]);
|
||||
|
||||
const getItemContent = useCallback(
|
||||
(_: number, log: ILog): JSX.Element => {
|
||||
if (options.format === 'raw') {
|
||||
@@ -130,6 +128,75 @@ function LogsExplorerList({
|
||||
],
|
||||
);
|
||||
|
||||
const renderContent = useMemo(() => {
|
||||
const components = isLoading
|
||||
? {
|
||||
Footer,
|
||||
}
|
||||
: {};
|
||||
|
||||
if (options.format === 'table') {
|
||||
return (
|
||||
<InfinityTableView
|
||||
ref={ref}
|
||||
isLoading={isLoading}
|
||||
tableViewProps={{
|
||||
logs,
|
||||
fields: selectedFields,
|
||||
linesPerRow: options.maxLines,
|
||||
fontSize: options.fontSize,
|
||||
appendTo: 'end',
|
||||
activeLogIndex,
|
||||
}}
|
||||
infitiyTableProps={{ onEndReached }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function getMarginTop(): string {
|
||||
switch (options.fontSize) {
|
||||
case FontSize.SMALL:
|
||||
return '10px';
|
||||
case FontSize.MEDIUM:
|
||||
return '12px';
|
||||
case FontSize.LARGE:
|
||||
return '15px';
|
||||
default:
|
||||
return '15px';
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
style={{ width: '100%', marginTop: getMarginTop() }}
|
||||
bodyStyle={CARD_BODY_STYLE}
|
||||
>
|
||||
<OverlayScrollbar isVirtuoso>
|
||||
<Virtuoso
|
||||
key={activeLogIndex || 'logs-virtuoso'}
|
||||
ref={ref}
|
||||
initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0}
|
||||
data={logs}
|
||||
endReached={onEndReached}
|
||||
totalCount={logs.length}
|
||||
itemContent={getItemContent}
|
||||
components={components}
|
||||
/>
|
||||
</OverlayScrollbar>
|
||||
</Card>
|
||||
);
|
||||
}, [
|
||||
isLoading,
|
||||
options.format,
|
||||
options.maxLines,
|
||||
options.fontSize,
|
||||
activeLogIndex,
|
||||
logs,
|
||||
onEndReached,
|
||||
getItemContent,
|
||||
selectedFields,
|
||||
]);
|
||||
|
||||
const isTraceToLogsNavigation = useMemo(() => {
|
||||
if (!currentStagedQueryData) return false;
|
||||
return isTraceToLogsQuery(currentStagedQueryData);
|
||||
@@ -169,83 +236,6 @@ function LogsExplorerList({
|
||||
return getEmptyLogsListConfig(handleClearFilters);
|
||||
}, [isTraceToLogsNavigation, handleClearFilters]);
|
||||
|
||||
const handleLoadMore = useCallback(() => {
|
||||
if (isLoading || isFetching) return;
|
||||
|
||||
onEndReached(logs.length);
|
||||
}, [isLoading, isFetching, onEndReached, logs.length]);
|
||||
|
||||
const renderContent = useMemo(() => {
|
||||
const components = isLoading
|
||||
? {
|
||||
Footer,
|
||||
}
|
||||
: {};
|
||||
|
||||
if (options.format === 'table') {
|
||||
return (
|
||||
<ColumnView
|
||||
logs={logs}
|
||||
onLoadMore={handleLoadMore}
|
||||
selectedFields={selectedFields}
|
||||
isLoading={isLoading}
|
||||
isFetching={isFetching}
|
||||
options={{
|
||||
maxLinesPerRow: options.maxLines,
|
||||
fontSize: options.fontSize,
|
||||
}}
|
||||
isFrequencyChartVisible={isFrequencyChartVisible}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function getMarginTop(): string {
|
||||
switch (options.fontSize) {
|
||||
case FontSize.SMALL:
|
||||
return '10px';
|
||||
case FontSize.MEDIUM:
|
||||
return '12px';
|
||||
case FontSize.LARGE:
|
||||
return '15px';
|
||||
default:
|
||||
return '15px';
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<InfinityWrapperStyled data-testid="logs-list-virtuoso">
|
||||
<Card
|
||||
style={{ width: '100%', marginTop: getMarginTop() }}
|
||||
bodyStyle={CARD_BODY_STYLE}
|
||||
>
|
||||
<OverlayScrollbar isVirtuoso>
|
||||
<Virtuoso
|
||||
key={activeLogIndex || 'logs-virtuoso'}
|
||||
ref={ref}
|
||||
initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0}
|
||||
data={logs}
|
||||
endReached={onEndReached}
|
||||
totalCount={logs.length}
|
||||
itemContent={getItemContent}
|
||||
components={components}
|
||||
/>
|
||||
</OverlayScrollbar>
|
||||
</Card>
|
||||
</InfinityWrapperStyled>
|
||||
);
|
||||
}, [
|
||||
isLoading,
|
||||
activeLogIndex,
|
||||
handleLoadMore,
|
||||
isFetching,
|
||||
logs,
|
||||
onEndReached,
|
||||
getItemContent,
|
||||
selectedFields,
|
||||
isFrequencyChartVisible,
|
||||
options,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="logs-list-view-container">
|
||||
{(isLoading || (isFetching && logs.length === 0)) && <LogsLoading />}
|
||||
@@ -274,7 +264,9 @@ function LogsExplorerList({
|
||||
|
||||
{!isLoading && !isError && logs.length > 0 && (
|
||||
<>
|
||||
{renderContent}
|
||||
<InfinityWrapperStyled data-testid="logs-list-virtuoso">
|
||||
{renderContent}
|
||||
</InfinityWrapperStyled>
|
||||
|
||||
<LogDetail
|
||||
selectedTab={VIEW_TYPES.OVERVIEW}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
padding-bottom: 10px;
|
||||
padding-bottom: 60px;
|
||||
|
||||
.views-tabs-container {
|
||||
padding: 8px 16px;
|
||||
@@ -216,19 +216,15 @@
|
||||
background-color: var(--bg-ink-500);
|
||||
}
|
||||
|
||||
.logs-frequency-chart-container {
|
||||
padding: 0px 8px;
|
||||
|
||||
.logs-frequency-chart {
|
||||
.ant-card-body {
|
||||
height: 140px;
|
||||
min-height: 140px;
|
||||
padding: 0 16px 22px 16px;
|
||||
font-family: 'Geist Mono';
|
||||
}
|
||||
|
||||
margin-bottom: 0px;
|
||||
.logs-histogram {
|
||||
.ant-card-body {
|
||||
height: 140px;
|
||||
min-height: 140px;
|
||||
padding: 0 16px 22px 16px;
|
||||
font-family: 'Geist Mono';
|
||||
}
|
||||
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
import './LogsExplorerViews.styles.scss';
|
||||
|
||||
import { Button, Switch, Typography } from 'antd';
|
||||
import getFromLocalstorage from 'api/browser/localstorage/get';
|
||||
import setToLocalstorage from 'api/browser/localstorage/set';
|
||||
import { getQueryStats, WsDataEvent } from 'api/common/getQueryStats';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||
@@ -105,13 +103,7 @@ function LogsExplorerViewsContainer({
|
||||
}): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [showFrequencyChart, setShowFrequencyChart] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const frequencyChart = getFromLocalstorage(LOCALSTORAGE.SHOW_FREQUENCY_CHART);
|
||||
setShowFrequencyChart(frequencyChart === 'true');
|
||||
}, []);
|
||||
const [showFrequencyChart, setShowFrequencyChart] = useState(true);
|
||||
|
||||
// this is to respect the panel type present in the URL rather than defaulting it to list always.
|
||||
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||
@@ -680,18 +672,6 @@ function LogsExplorerViewsContainer({
|
||||
[logs, timezone.value],
|
||||
);
|
||||
|
||||
const handleToggleFrequencyChart = useCallback(() => {
|
||||
const newShowFrequencyChart = !showFrequencyChart;
|
||||
|
||||
// store the value in local storage
|
||||
setToLocalstorage(
|
||||
LOCALSTORAGE.SHOW_FREQUENCY_CHART,
|
||||
newShowFrequencyChart?.toString() || 'false',
|
||||
);
|
||||
|
||||
setShowFrequencyChart(newShowFrequencyChart);
|
||||
}, [showFrequencyChart]);
|
||||
|
||||
return (
|
||||
<div className="logs-explorer-views-container">
|
||||
<div className="logs-explorer-views-types">
|
||||
@@ -705,7 +685,7 @@ function LogsExplorerViewsContainer({
|
||||
size="small"
|
||||
checked={showFrequencyChart}
|
||||
defaultChecked
|
||||
onChange={handleToggleFrequencyChart}
|
||||
onChange={(): void => setShowFrequencyChart(!showFrequencyChart)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -781,14 +761,12 @@ function LogsExplorerViewsContainer({
|
||||
</div>
|
||||
|
||||
{selectedPanelType === PANEL_TYPES.LIST && showFrequencyChart && (
|
||||
<div className="logs-frequency-chart-container">
|
||||
<LogsExplorerChart
|
||||
className="logs-frequency-chart"
|
||||
isLoading={isFetchingListChartData || isLoadingListChartData}
|
||||
data={chartData}
|
||||
isLogsExplorerViews={panelType === PANEL_TYPES.LIST}
|
||||
/>
|
||||
</div>
|
||||
<LogsExplorerChart
|
||||
className="logs-histogram"
|
||||
isLoading={isFetchingListChartData || isLoadingListChartData}
|
||||
data={chartData}
|
||||
isLogsExplorerViews={panelType === PANEL_TYPES.LIST}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="logs-explorer-views-type-content">
|
||||
@@ -799,12 +777,12 @@ function LogsExplorerViewsContainer({
|
||||
currentStagedQueryData={listQuery}
|
||||
logs={logs}
|
||||
onEndReached={handleEndReached}
|
||||
isFrequencyChartVisible={showFrequencyChart}
|
||||
isError={isError}
|
||||
error={error as APIError}
|
||||
isFilterApplied={!isEmpty(listQuery?.filters?.items)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedPanelType === PANEL_TYPES.TIME_SERIES && (
|
||||
<TimeSeriesView
|
||||
isLoading={isLoading || isFetching}
|
||||
|
||||
@@ -65,6 +65,17 @@ const useOptionsMenu = ({
|
||||
const [isFocused, setIsFocused] = useState<boolean>(false);
|
||||
const debouncedSearchText = useDebounce(searchText, 300);
|
||||
|
||||
// const initialQueryParams = useMemo(
|
||||
// () => ({
|
||||
// searchText: '',
|
||||
// aggregateAttribute: '',
|
||||
// tagType: undefined,
|
||||
// dataSource,
|
||||
// aggregateOperator,
|
||||
// }),
|
||||
// [dataSource, aggregateOperator],
|
||||
// );
|
||||
|
||||
const initialQueryParamsV5: QueryKeyRequestProps = useMemo(
|
||||
() => ({
|
||||
signal: dataSource,
|
||||
@@ -78,6 +89,22 @@ const useOptionsMenu = ({
|
||||
redirectWithQuery: redirectWithOptionsData,
|
||||
} = useUrlQueryData<OptionsQuery>(URL_OPTIONS, defaultOptionsQuery);
|
||||
|
||||
// const initialQueries = useMemo(
|
||||
// () =>
|
||||
// initialOptions?.selectColumns?.map((column) => ({
|
||||
// queryKey: column,
|
||||
// queryFn: (): Promise<
|
||||
// SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
|
||||
// > =>
|
||||
// getAggregateKeys({
|
||||
// ...initialQueryParams,
|
||||
// searchText: column,
|
||||
// }),
|
||||
// enabled: !!column && !optionsQuery,
|
||||
// })) || [],
|
||||
// [initialOptions?.selectColumns, initialQueryParams, optionsQuery],
|
||||
// );
|
||||
|
||||
const initialQueriesV5 = useMemo(
|
||||
() =>
|
||||
initialOptions?.selectColumns?.map((column) => ({
|
||||
|
||||
@@ -42,11 +42,11 @@
|
||||
gap: 8px;
|
||||
|
||||
&.active-tab {
|
||||
background-color: var(--bg-robin-500);
|
||||
border-bottom: 1px solid var(--bg-robin-500);
|
||||
background-color: var(--bg-ink-500);
|
||||
border-bottom: 1px solid var(--bg-ink-500);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-robin-500) !important;
|
||||
background-color: var(--bg-ink-500) !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
Unplug,
|
||||
User,
|
||||
UserPlus,
|
||||
Video,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { SecondaryMenuItemKey, SidebarItem } from './sideNav.types';
|
||||
@@ -103,7 +104,6 @@ const menuItems: SidebarItem[] = [
|
||||
icon: <HardDrive size={16} />,
|
||||
itemKey: 'services',
|
||||
},
|
||||
|
||||
{
|
||||
key: ROUTES.LOGS,
|
||||
label: 'Logs',
|
||||
@@ -274,6 +274,13 @@ export const defaultMoreMenuItems: SidebarItem[] = [
|
||||
isBeta: true,
|
||||
itemKey: 'meter-explorer',
|
||||
},
|
||||
{
|
||||
key: ROUTES.SESSION_RECORDINGS,
|
||||
label: 'Session Recordings',
|
||||
icon: <Video size={16} />,
|
||||
isEnabled: true,
|
||||
itemKey: 'session-recordings',
|
||||
},
|
||||
{
|
||||
key: ROUTES.MESSAGING_QUEUES_OVERVIEW,
|
||||
label: 'Messaging Queues',
|
||||
|
||||
@@ -237,6 +237,8 @@ export const routesToSkip = [
|
||||
ROUTES.METER,
|
||||
ROUTES.METER_EXPLORER_VIEWS,
|
||||
ROUTES.SOMETHING_WENT_WRONG,
|
||||
ROUTES.SESSION_RECORDINGS,
|
||||
ROUTES.SESSION_RECORDINGS_DETAIL,
|
||||
];
|
||||
|
||||
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];
|
||||
|
||||
@@ -12,7 +12,6 @@ export type UseCopyLogLink = {
|
||||
isLogsExplorerPage: boolean;
|
||||
activeLogId: string | null;
|
||||
onLogCopy: MouseEventHandler<HTMLElement>;
|
||||
onClearActiveLog: () => void;
|
||||
};
|
||||
|
||||
export type UseActiveLog = {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { toast } from '@signozhq/sonner';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import {
|
||||
@@ -22,10 +21,9 @@ import { UseCopyLogLink } from './types';
|
||||
|
||||
export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
|
||||
const urlQuery = useUrlQuery();
|
||||
const { pathname, search } = useLocation();
|
||||
const { pathname } = useLocation();
|
||||
const [, setCopy] = useCopyToClipboard();
|
||||
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const { queryData: activeLogId } = useUrlQueryData<string | null>(
|
||||
QueryParams.activeLogId,
|
||||
@@ -60,19 +58,13 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
|
||||
const link = `${window.location.origin}${pathname}?${urlQuery.toString()}`;
|
||||
|
||||
setCopy(link);
|
||||
|
||||
toast.success('Copied to clipboard', { position: 'top-right' });
|
||||
notifications.success({
|
||||
message: 'Copied to clipboard',
|
||||
});
|
||||
},
|
||||
[logId, urlQuery, minTime, maxTime, pathname, setCopy],
|
||||
[logId, urlQuery, minTime, maxTime, pathname, setCopy, notifications],
|
||||
);
|
||||
|
||||
const onClearActiveLog = useCallback(() => {
|
||||
const currentUrlQuery = new URLSearchParams(search);
|
||||
currentUrlQuery.delete(QueryParams.activeLogId);
|
||||
const newUrl = `${pathname}?${currentUrlQuery.toString()}`;
|
||||
safeNavigate(newUrl);
|
||||
}, [pathname, search, safeNavigate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActiveLog) return;
|
||||
|
||||
@@ -89,6 +81,5 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
|
||||
isLogsExplorerPage,
|
||||
activeLogId,
|
||||
onLogCopy,
|
||||
onClearActiveLog,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -40,13 +40,6 @@ const useDragColumns = <T>(storageKey: LOCALSTORAGE): UseDragColumns<T> => {
|
||||
[handleRedirectWithDraggedColumns],
|
||||
);
|
||||
|
||||
const onColumnOrderChange = useCallback(
|
||||
(newColumns: ColumnsType<T>): void => {
|
||||
handleRedirectWithDraggedColumns(newColumns);
|
||||
},
|
||||
[handleRedirectWithDraggedColumns],
|
||||
);
|
||||
|
||||
const redirectWithNewDraggedColumns = useCallback(
|
||||
async (localStorageColumns: string) => {
|
||||
let nextDraggedColumns: ColumnsType<T> = [];
|
||||
@@ -76,7 +69,6 @@ const useDragColumns = <T>(storageKey: LOCALSTORAGE): UseDragColumns<T> => {
|
||||
return {
|
||||
draggedColumns,
|
||||
onDragColumns,
|
||||
onColumnOrderChange,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -7,5 +7,4 @@ export type UseDragColumns<T> = {
|
||||
fromIndex: number,
|
||||
toIndex: number,
|
||||
) => void;
|
||||
onColumnOrderChange: (newColumns: ColumnsType<T>) => void;
|
||||
};
|
||||
|
||||
@@ -105,6 +105,7 @@ const logsQueryServerRequest = (): void =>
|
||||
describe('Logs Explorer Tests', () => {
|
||||
test('Logs Explorer default view test without data', async () => {
|
||||
const {
|
||||
getByText,
|
||||
getByRole,
|
||||
queryByText,
|
||||
getByTestId,
|
||||
@@ -122,10 +123,13 @@ describe('Logs Explorer Tests', () => {
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
// by default is hidden, toggle the chart and check it's visibility
|
||||
// check the presence of frequency chart content
|
||||
expect(getByText(frequencyChartContent)).toBeInTheDocument();
|
||||
|
||||
// toggle the chart and check it gets removed from the DOM
|
||||
const histogramToggle = getByRole('switch');
|
||||
fireEvent.click(histogramToggle);
|
||||
expect(queryByText(frequencyChartContent)).toBeInTheDocument();
|
||||
expect(queryByText(frequencyChartContent)).not.toBeInTheDocument();
|
||||
|
||||
// check the presence of search bar and query builder and absence of clickhouse
|
||||
const searchView = getByTestId('search-view');
|
||||
@@ -273,10 +277,10 @@ describe('Logs Explorer Tests', () => {
|
||||
const histogramToggle = getByRole('switch');
|
||||
expect(histogramToggle).toBeInTheDocument();
|
||||
expect(histogramToggle).toBeChecked();
|
||||
expect(queryByText(frequencyChartContent)).toBeInTheDocument();
|
||||
|
||||
// toggle the chart and check it gets removed from the DOM
|
||||
await fireEvent.click(histogramToggle);
|
||||
expect(histogramToggle).not.toBeChecked();
|
||||
expect(queryByText(frequencyChartContent)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
82
frontend/src/pages/SessionRecording/README.md
Normal file
82
frontend/src/pages/SessionRecording/README.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# Session Recordings Page
|
||||
|
||||
A minimal, focused page that displays session recordings with just session names and play buttons.
|
||||
|
||||
## Features
|
||||
|
||||
- **Simple Session List**: Clean table showing only session names and play buttons
|
||||
- **Direct Access**: Click play button or row to open session recording
|
||||
- **Minimal UI**: No filters, search, or extra information - just the essentials
|
||||
- **Responsive Design**: Adapts to different screen sizes
|
||||
- **Dark/Light Mode Support**: Follows the app's theme system
|
||||
|
||||
## Components
|
||||
|
||||
### Main Component
|
||||
|
||||
- `index.tsx` - Main session recordings page component
|
||||
|
||||
### Supporting Files
|
||||
|
||||
- `types.ts` - TypeScript interfaces for session recording data
|
||||
- `styles.scss` - Minimal styling with theme support
|
||||
- `README.md` - This documentation file
|
||||
|
||||
## Data Structure
|
||||
|
||||
Each session recording includes:
|
||||
|
||||
- Basic identification (ID, session ID)
|
||||
- User information (name, user agent)
|
||||
- Timing details (start time, duration)
|
||||
- Geographic data (country, city)
|
||||
- Technical details (device, browser, OS)
|
||||
- Status information (completion status, error flags)
|
||||
- Recording URL for playback
|
||||
|
||||
## Table Columns
|
||||
|
||||
1. **Session Name** - Session identifier
|
||||
2. **Actions** - Play button to open session recording
|
||||
|
||||
## Usage
|
||||
|
||||
The page automatically loads with mock data for demonstration. In production, replace the mock data with actual API calls to fetch session recordings.
|
||||
|
||||
### Table Interaction
|
||||
|
||||
- Click on any row to open the session recording
|
||||
- Use the play button in the Actions column for quick access
|
||||
- Sort by session name by clicking the column header
|
||||
- Navigate through pages using the pagination controls
|
||||
|
||||
## Styling
|
||||
|
||||
The page uses CSS custom properties for theming:
|
||||
|
||||
- Dark mode: Uses `--bg-ink-*` and `--bg-slate-*` color variables
|
||||
- Light mode: Uses `--bg-vanilla-*` color variables
|
||||
- Accent colors: Uses `--bg-sakura-*` for primary actions and highlights
|
||||
|
||||
## Responsive Behavior
|
||||
|
||||
- **Desktop**: Clean table layout with proper spacing
|
||||
- **Tablet**: Responsive table with maintained readability
|
||||
- **Mobile**: Single-column layout with touch-friendly buttons
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
The UI follows an extremely minimalist approach:
|
||||
|
||||
- Only essential information displayed
|
||||
- No visual clutter or unnecessary features
|
||||
- Focus on quick access to session recordings
|
||||
- Clean, readable table layout
|
||||
- Consistent with the app's design system
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Session count display
|
||||
- Basic sorting options
|
||||
- Export functionality
|
||||
- Real-time updates for active sessions
|
||||
80
frontend/src/pages/SessionRecording/RRWebPlayer.tsx
Normal file
80
frontend/src/pages/SessionRecording/RRWebPlayer.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import Player, { RRwebPlayerOptions } from 'rrweb-player';
|
||||
import { eventWithTime } from '@rrweb/types';
|
||||
import 'rrweb-player/dist/style.css'; // Import the styles for the player
|
||||
|
||||
interface RRWebPlayerProps {
|
||||
events: eventWithTime[];
|
||||
options?: Partial<RRwebPlayerOptions['props']>;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
const RRWebPlayer: React.FC<RRWebPlayerProps> = ({
|
||||
events,
|
||||
options = {},
|
||||
className = '',
|
||||
style = {},
|
||||
}) => {
|
||||
const playerRef = useRef<HTMLDivElement>(null);
|
||||
const playerInstanceRef = useRef<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!playerRef.current || !events || events.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clean up previous instance if it exists
|
||||
if (playerInstanceRef.current) {
|
||||
// Remove the previous player element
|
||||
if (playerRef.current) {
|
||||
playerRef.current.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Create new player instance using the imported Player
|
||||
playerInstanceRef.current = new Player({
|
||||
target: playerRef.current as HTMLElement,
|
||||
props: {
|
||||
events,
|
||||
...options,
|
||||
},
|
||||
});
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
if (playerInstanceRef.current && playerRef.current) {
|
||||
playerRef.current.innerHTML = '';
|
||||
playerInstanceRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [events, options]);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (playerInstanceRef.current && playerRef.current) {
|
||||
playerRef.current.innerHTML = '';
|
||||
playerInstanceRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!events || events.length === 0) {
|
||||
return (
|
||||
<div className={`rrweb-player-empty ${className}`} style={style}>
|
||||
<p>No session events available</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={playerRef}
|
||||
className={`rrweb-player-container ${className}`}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default RRWebPlayer;
|
||||
129
frontend/src/pages/SessionRecording/SessionDetail/index.tsx
Normal file
129
frontend/src/pages/SessionRecording/SessionDetail/index.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Button, Typography } from 'antd';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { useParams, useHistory } from 'react-router-dom';
|
||||
import { useQuery } from 'react-query';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { getAttributesValues } from 'api/queryBuilder/getAttributesValues';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { SessionRecording } from '../types';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { PANEL_TYPES, initialQueriesMap } from 'constants/queryBuilder';
|
||||
import RRWebPlayer from '../RRWebPlayer';
|
||||
import React from 'react';
|
||||
|
||||
import './styles.scss';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
|
||||
export default function SessionDetail(): JSX.Element {
|
||||
const { stagedQuery } = useQueryBuilder();
|
||||
const { sessionId } = useParams<{ sessionId: string }>();
|
||||
const history = useHistory();
|
||||
|
||||
// Fetch session-related logs data using useGetQueryRange
|
||||
const {
|
||||
data: sessionLogsData,
|
||||
isLoading: isSessionLogsLoading,
|
||||
} = useGetQueryRange(
|
||||
{
|
||||
query: stagedQuery || initialQueriesMap.logs,
|
||||
graphType: PANEL_TYPES.LIST,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
globalSelectedInterval: '3d',
|
||||
params: {
|
||||
dataSource: DataSource.LOGS,
|
||||
},
|
||||
formatForWeb: false,
|
||||
},
|
||||
ENTITY_VERSION_V5,
|
||||
{
|
||||
queryKey: ['sessionLogs', sessionId],
|
||||
enabled: !!sessionId && !!stagedQuery,
|
||||
},
|
||||
);
|
||||
|
||||
console.log({ sessionLogsData });
|
||||
|
||||
// Extract body fields from session logs data
|
||||
const sessionEvents = React.useMemo(() => {
|
||||
if (!sessionLogsData?.payload?.data?.newResult?.data?.result?.[0]?.list) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return sessionLogsData.payload.data.newResult.data.result[0].list
|
||||
.map((row: any) => {
|
||||
// Try to extract body field from different possible locations
|
||||
const body =
|
||||
row.data?.body || row.data?.message || row.data?.log || row.data;
|
||||
|
||||
// If body is a string, try to parse it as JSON
|
||||
if (typeof body === 'string') {
|
||||
try {
|
||||
return JSON.parse(body);
|
||||
} catch {
|
||||
// If parsing fails, return the string as is
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
return body;
|
||||
})
|
||||
.filter(Boolean); // Remove any undefined/null values
|
||||
}, [sessionLogsData]);
|
||||
|
||||
const handleBack = (): void => {
|
||||
history.push(ROUTES.SESSION_RECORDINGS);
|
||||
};
|
||||
|
||||
return (
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<div className="session-detail-page">
|
||||
<div className="page-header">
|
||||
<div className="header-content">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ArrowLeft size={16} />}
|
||||
onClick={handleBack}
|
||||
className="back-button"
|
||||
>
|
||||
Back to Sessions
|
||||
</Button>
|
||||
<Title level={2} className="page-title">
|
||||
Session Recording: {sessionId}
|
||||
</Title>
|
||||
<Text type="secondary" className="page-description">
|
||||
Detailed view of session recording and metadata
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="page-content">
|
||||
{/* Display RRWebPlayer with session events */}
|
||||
{isSessionLogsLoading ? (
|
||||
<div>Loading session logs...</div>
|
||||
) : sessionEvents.length > 0 ? (
|
||||
<div>
|
||||
<h3>Session Recording Player</h3>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<strong>Total Events: {sessionEvents.length}</strong>
|
||||
</div>
|
||||
<RRWebPlayer
|
||||
events={sessionEvents}
|
||||
options={{
|
||||
autoPlay: false,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div>No session events available</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Sentry.ErrorBoundary>
|
||||
);
|
||||
}
|
||||
306
frontend/src/pages/SessionRecording/SessionDetail/styles.scss
Normal file
306
frontend/src/pages/SessionRecording/SessionDetail/styles.scss
Normal file
@@ -0,0 +1,306 @@
|
||||
.session-detail-page {
|
||||
height: 100%;
|
||||
background: var(--bg-ink-400);
|
||||
padding: 0;
|
||||
|
||||
.page-header {
|
||||
background: var(--bg-ink-500);
|
||||
border-bottom: 1px solid var(--bg-slate-400);
|
||||
padding: 24px 24px 16px;
|
||||
margin-bottom: 0;
|
||||
|
||||
.header-content {
|
||||
.back-button {
|
||||
margin-bottom: 16px;
|
||||
color: var(--bg-vanilla-400);
|
||||
padding: 0;
|
||||
height: auto;
|
||||
font-size: 14px;
|
||||
|
||||
&:hover {
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
.anticon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0 0 8px 0;
|
||||
color: var(--bg-vanilla-100);
|
||||
font-weight: 600;
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.page-description {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: var(--bg-vanilla-400);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-content {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.content-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: 16px;
|
||||
|
||||
.overview-card,
|
||||
.user-card,
|
||||
.device-card,
|
||||
.player-card,
|
||||
.logs-card {
|
||||
background: var(--bg-ink-500);
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
border-radius: 6px;
|
||||
|
||||
.ant-card-head {
|
||||
background: var(--bg-ink-400);
|
||||
border-bottom: 1px solid var(--bg-slate-400);
|
||||
|
||||
.ant-card-head-title {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.ant-descriptions {
|
||||
.ant-descriptions-item-label {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.ant-descriptions-item-content {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.user-agent-text {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.logs-content {
|
||||
.logs-count {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ant-typography {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
line-height: 16px;
|
||||
word-break: break-all;
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ant-tag {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ant-space {
|
||||
.anticon {
|
||||
color: var(--bg-vanilla-400);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.player-card {
|
||||
grid-column: span 2;
|
||||
|
||||
.player-content {
|
||||
.player-description {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
margin-bottom: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.player-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.play-button-large {
|
||||
background: var(--bg-sakura-500);
|
||||
border-color: var(--bg-sakura-500);
|
||||
color: var(--bg-ink-500);
|
||||
height: 48px;
|
||||
padding: 0 32px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
border-radius: 8px;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-sakura-600);
|
||||
border-color: var(--bg-sakura-600);
|
||||
}
|
||||
|
||||
.anticon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Light mode overrides
|
||||
.lightMode {
|
||||
.session-detail-page {
|
||||
background: var(--bg-vanilla-100);
|
||||
|
||||
.page-header {
|
||||
background: var(--bg-vanilla-200);
|
||||
border-bottom: 1px solid var(--bg-slate-300);
|
||||
|
||||
.header-content {
|
||||
.back-button {
|
||||
color: var(--bg-ink-400);
|
||||
|
||||
&:hover {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
|
||||
.page-title {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
|
||||
.page-description {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-content {
|
||||
.content-grid {
|
||||
.overview-card,
|
||||
.user-card,
|
||||
.device-card,
|
||||
.player-card {
|
||||
background: var(--bg-vanilla-200);
|
||||
border: 1px solid var(--bg-slate-300);
|
||||
|
||||
.ant-card-head {
|
||||
background: var(--bg-vanilla-300);
|
||||
border-bottom: 1px solid var(--bg-slate-300);
|
||||
|
||||
.ant-card-head-title {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-descriptions {
|
||||
.ant-descriptions-item-label {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.ant-descriptions-item-content {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-space {
|
||||
.anticon {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.player-card {
|
||||
.player-content {
|
||||
.player-description {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.session-detail-page {
|
||||
.page-content {
|
||||
padding: 16px;
|
||||
|
||||
.content-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
|
||||
.player-card {
|
||||
grid-column: span 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.session-detail-page {
|
||||
.page-header {
|
||||
padding: 16px 16px 12px;
|
||||
|
||||
.header-content {
|
||||
.page-title {
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-content {
|
||||
padding: 12px;
|
||||
|
||||
.content-grid {
|
||||
gap: 12px;
|
||||
|
||||
.overview-card,
|
||||
.user-card,
|
||||
.device-card,
|
||||
.player-card {
|
||||
.ant-card-body {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.ant-descriptions {
|
||||
.ant-descriptions-item {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
166
frontend/src/pages/SessionRecording/index.tsx
Normal file
166
frontend/src/pages/SessionRecording/index.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Button, Card, Typography } from 'antd';
|
||||
import { PlayCircle } from 'lucide-react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import { getAttributesValues } from 'api/queryBuilder/getAttributesValues';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { SessionRecording } from './types';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import './styles.scss';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
export default function SessionRecordings(): JSX.Element {
|
||||
const history = useHistory();
|
||||
|
||||
// Use React Query to fetch session attributes
|
||||
const { data: sessionAttributes, isLoading, error } = useQuery({
|
||||
queryKey: ['sessionAttributes', 'rum.sessionId'],
|
||||
queryFn: () =>
|
||||
getAttributesValues({
|
||||
aggregateOperator: 'noop',
|
||||
dataSource: DataSource.LOGS,
|
||||
aggregateAttribute: '',
|
||||
attributeKey: 'rum.sessionId',
|
||||
searchText: '',
|
||||
filterAttributeKeyDataType: DataTypes.String,
|
||||
tagType: 'resource',
|
||||
}),
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
cacheTime: 10 * 60 * 1000, // 10 minutes
|
||||
});
|
||||
|
||||
const getQueryParams = (sessionId: string) => {
|
||||
const compositeQuery = `compositeQuery=%257B%2522queryType%2522%253A%2522builder%2522%252C%2522builder%2522%253A%257B%2522queryData%2522%253A%255B%257B%2522dataSource%2522%253A%2522logs%2522%252C%2522queryName%2522%253A%2522A%2522%252C%2522aggregateOperator%2522%253A%2522count%2522%252C%2522aggregateAttribute%2522%253A%257B%2522id%2522%253A%2522----%2522%252C%2522dataType%2522%253A%2522%2522%252C%2522key%2522%253A%2522%2522%252C%2522type%2522%253A%2522%2522%257D%252C%2522timeAggregation%2522%253A%2522rate%2522%252C%2522spaceAggregation%2522%253A%2522sum%2522%252C%2522filter%2522%253A%257B%2522expression%2522%253A%2522rum.sessionId%2520%253D%2520%27${sessionId}%27%2520%2522%257D%252C%2522aggregations%2522%253A%255B%257B%2522expression%2522%253A%2522count%28%29%2520%2522%257D%255D%252C%2522functions%2522%253A%255B%255D%252C%2522filters%2522%253A%257B%2522items%2522%253A%255B%255D%252C%2522op%2522%253A%2522AND%2522%257D%252C%2522expression%2522%253A%2522A%2522%252C%2522disabled%2522%253Afalse%252C%2522stepInterval%2522%253Anull%252C%2522having%2522%253A%257B%2522expression%2522%253A%2522%2522%257D%252C%2522limit%2522%253Anull%252C%2522orderBy%2522%253A%255B%255D%252C%2522groupBy%2522%253A%255B%255D%252C%2522legend%2522%253A%2522%2522%252C%2522reduceTo%2522%253A%2522avg%2522%252C%2522source%2522%253A%2522%2522%257D%255D%252C%2522queryFormulas%2522%253A%255B%255D%257D%252C%2522promql%2522%253A%255B%257B%2522name%2522%253A%2522A%2522%252C%2522query%2522%253A%2522%2522%252C%2522legend%2522%253A%2522%2522%252C%2522disabled%2522%253Afalse%257D%255D%252C%2522clickhouse_sql%2522%253A%255B%257B%2522name%2522%253A%2522A%2522%252C%2522legend%2522%253A%2522%2522%252C%2522disabled%2522%253Afalse%252C%2522query%2522%253A%2522%2522%257D%255D%252C%2522id%2522%253A%25226a102b3b-cb6b-4409-9e57-317f9ecb941b%2522%257D&options=%7B%22selectColumns%22%3A%5B%7B%22name%22%3A%22timestamp%22%2C%22signal%22%3A%22logs%22%2C%22fieldContext%22%3A%22log%22%2C%22fieldDataType%22%3A%22%22%2C%22isIndexed%22%3Afalse%7D%2C%7B%22name%22%3A%22body%22%2C%22signal%22%3A%22logs%22%2C%22fieldContext%22%3A%22log%22%2C%22fieldDataType%22%3A%22%22%2C%22isIndexed%22%3Afalse%7D%5D%2C%22maxLines%22%3A2%2C%22format%22%3A%22raw%22%2C%22fontSize%22%3A%22small%22%7D`;
|
||||
return compositeQuery;
|
||||
};
|
||||
|
||||
// Transform API response to table data
|
||||
const sessionRecordings: SessionRecording[] = useMemo(() => {
|
||||
if (
|
||||
sessionAttributes?.statusCode === 200 &&
|
||||
sessionAttributes.payload?.stringAttributeValues
|
||||
) {
|
||||
return sessionAttributes.payload.stringAttributeValues.map(
|
||||
(sessionId: string, index: number) => ({
|
||||
id: String(index + 1),
|
||||
sessionId,
|
||||
userName: 'anonymous',
|
||||
userAgent: 'Mozilla/5.0 (Unknown)',
|
||||
startTime: new Date().toISOString(), // You can extract timestamp from sessionId if available
|
||||
duration: 0,
|
||||
pageViews: 0,
|
||||
country: 'Unknown',
|
||||
city: 'Unknown',
|
||||
device: 'Unknown',
|
||||
browser: 'Unknown',
|
||||
os: 'Unknown',
|
||||
status: 'completed' as const,
|
||||
hasErrors: false,
|
||||
recordingUrl: `/session/${sessionId}?${getQueryParams(sessionId)}`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return [];
|
||||
}, [sessionAttributes]);
|
||||
|
||||
// Log the response for debugging
|
||||
if (sessionAttributes && sessionAttributes.statusCode === 200) {
|
||||
console.log('Session attributes fetched:', sessionAttributes.payload);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching session attributes:', error);
|
||||
}
|
||||
|
||||
const handleSessionClick = (record: SessionRecording): void => {
|
||||
history.push(
|
||||
`/session-recordings/${record.sessionId}?${getQueryParams(
|
||||
record.sessionId,
|
||||
)}`,
|
||||
);
|
||||
};
|
||||
|
||||
const handlePlayClick = (
|
||||
e: React.MouseEvent,
|
||||
record: SessionRecording,
|
||||
): void => {
|
||||
e.stopPropagation();
|
||||
history.push(
|
||||
`/session-recordings/${record.sessionId}?${getQueryParams(
|
||||
record.sessionId,
|
||||
)}`,
|
||||
);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'Session Name',
|
||||
dataIndex: 'sessionId',
|
||||
key: 'sessionId',
|
||||
width: 200,
|
||||
render: (sessionId: string) => <Text strong>{sessionId}</Text>,
|
||||
},
|
||||
{
|
||||
title: 'Actions',
|
||||
key: 'actions',
|
||||
width: 100,
|
||||
render: (_: any, record: SessionRecording) => (
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<PlayCircle size={14} />}
|
||||
onClick={(e) => handlePlayClick(e, record)}
|
||||
className="play-button"
|
||||
>
|
||||
Play
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<div className="session-recordings-page">
|
||||
<div className="page-header">
|
||||
<div className="header-content">
|
||||
<Title level={2} className="page-title">
|
||||
Session Recordings
|
||||
</Title>
|
||||
<Text type="secondary" className="page-description">
|
||||
Click play to view session recordings
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="page-content">
|
||||
<Card className="table-card">
|
||||
<ResizeTable
|
||||
columns={columns}
|
||||
dataSource={sessionRecordings}
|
||||
rowKey="id"
|
||||
loading={isLoading}
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total, range) =>
|
||||
`${range[0]}-${range[1]} of ${total} recordings`,
|
||||
}}
|
||||
className="session-recordings-table"
|
||||
onRow={(record) => ({
|
||||
onClick: () => handleSessionClick(record),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</Sentry.ErrorBoundary>
|
||||
);
|
||||
}
|
||||
221
frontend/src/pages/SessionRecording/styles.scss
Normal file
221
frontend/src/pages/SessionRecording/styles.scss
Normal file
@@ -0,0 +1,221 @@
|
||||
.session-recordings-page {
|
||||
height: 100%;
|
||||
background: var(--bg-ink-400);
|
||||
padding: 0;
|
||||
|
||||
.page-header {
|
||||
background: var(--bg-ink-500);
|
||||
border-bottom: 1px solid var(--bg-slate-400);
|
||||
padding: 24px 24px 16px;
|
||||
margin-bottom: 0;
|
||||
|
||||
.header-content {
|
||||
.page-title {
|
||||
margin: 0 0 8px 0;
|
||||
color: var(--bg-vanilla-100);
|
||||
font-weight: 600;
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.page-description {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: var(--bg-vanilla-400);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-content {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.table-card {
|
||||
background: var(--bg-ink-500);
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
border-radius: 6px;
|
||||
flex: 1;
|
||||
|
||||
.ant-card-body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.session-recordings-table {
|
||||
.ant-table {
|
||||
background: transparent;
|
||||
|
||||
.ant-table-thead > tr > th {
|
||||
background: var(--bg-ink-400);
|
||||
border-bottom: 1px solid var(--bg-slate-400);
|
||||
color: var(--bg-vanilla-300);
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr > td {
|
||||
background: transparent;
|
||||
border-bottom: 1px solid var(--bg-slate-400);
|
||||
padding: 16px;
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr:hover > td {
|
||||
background: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.clickable-row {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-ink-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pagination {
|
||||
margin: 16px 0 0 0;
|
||||
text-align: right;
|
||||
|
||||
.ant-pagination-item,
|
||||
.ant-pagination-prev,
|
||||
.ant-pagination-next {
|
||||
background: var(--bg-ink-400);
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
color: var(--bg-vanilla-400);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-sakura-500);
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
&.ant-pagination-item-active {
|
||||
background: var(--bg-sakura-500);
|
||||
border-color: var(--bg-sakura-500);
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pagination-options {
|
||||
.ant-select {
|
||||
.ant-select-selector {
|
||||
background: var(--bg-ink-400);
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Play button styles
|
||||
.play-button {
|
||||
background: var(--bg-sakura-500);
|
||||
border-color: var(--bg-sakura-500);
|
||||
color: var(--bg-ink-500);
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-sakura-600);
|
||||
border-color: var(--bg-sakura-600);
|
||||
}
|
||||
|
||||
.anticon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// Light mode overrides
|
||||
.lightMode {
|
||||
.session-recordings-page {
|
||||
background: var(--bg-vanilla-100);
|
||||
|
||||
.page-header {
|
||||
background: var(--bg-vanilla-200);
|
||||
border-bottom: 1px solid var(--bg-slate-300);
|
||||
|
||||
.header-content {
|
||||
.page-title {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
|
||||
.page-description {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-content {
|
||||
.table-card {
|
||||
background: var(--bg-vanilla-200);
|
||||
border: 1px solid var(--bg-slate-300);
|
||||
|
||||
.session-recordings-table {
|
||||
.ant-table {
|
||||
.ant-table-thead > tr > th {
|
||||
background: var(--bg-vanilla-300);
|
||||
border-bottom: 1px solid var(--bg-slate-300);
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr > td {
|
||||
border-bottom: 1px solid var(--bg-slate-300);
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr:hover > td {
|
||||
background: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
.clickable-row:hover {
|
||||
background: var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pagination {
|
||||
.ant-pagination-item,
|
||||
.ant-pagination-prev,
|
||||
.ant-pagination-next {
|
||||
background: var(--bg-vanilla-300);
|
||||
border: 1px solid var(--bg-slate-300);
|
||||
color: var(--bg-ink-400);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-sakura-500);
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pagination-options {
|
||||
.ant-select {
|
||||
.ant-select-selector {
|
||||
background: var(--bg-vanilla-300);
|
||||
border: 1px solid var(--bg-slate-300);
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.session-recordings-page {
|
||||
.page-content {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
17
frontend/src/pages/SessionRecording/types.ts
Normal file
17
frontend/src/pages/SessionRecording/types.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export interface SessionRecording {
|
||||
id: string;
|
||||
sessionId: string;
|
||||
userName: string;
|
||||
userAgent: string;
|
||||
startTime: string;
|
||||
duration: number; // seconds
|
||||
pageViews: number;
|
||||
country: string;
|
||||
city: string;
|
||||
device: string;
|
||||
browser: string;
|
||||
os: string;
|
||||
status: 'completed' | 'in_progress' | 'failed';
|
||||
hasErrors: boolean;
|
||||
recordingUrl: string;
|
||||
}
|
||||
@@ -108,6 +108,12 @@ const config = {
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
include: /node_modules/,
|
||||
use: [styleLoader, cssLoader],
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
exclude: /node_modules/,
|
||||
use: [
|
||||
styleLoader,
|
||||
{
|
||||
|
||||
@@ -4046,6 +4046,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.20.0.tgz#03554155b45d8b529adf635b2f6ad1165d70d8b4"
|
||||
integrity sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==
|
||||
|
||||
"@rrweb/types@2.0.0-alpha.18", "@rrweb/types@^2.0.0-alpha.4":
|
||||
version "2.0.0-alpha.18"
|
||||
resolved "https://registry.yarnpkg.com/@rrweb/types/-/types-2.0.0-alpha.18.tgz#e1d9af844cebbf30a2be8808f6cf64f5df3e7f50"
|
||||
integrity sha512-iMH3amHthJZ9x3gGmBPmdfim7wLGygC2GciIkw2A6SO8giSn8PHYtRT8OKNH4V+k3SZ6RSnYHcTQxBA7pSWZ3Q==
|
||||
|
||||
"@sentry-internal/browser-utils@8.41.0":
|
||||
version "8.41.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.41.0.tgz#9dc30a8c88aa6e1e542e5acae29ceabd1b377cc4"
|
||||
@@ -4284,38 +4289,6 @@
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@signozhq/sonner@0.1.0":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/sonner/-/sonner-0.1.0.tgz#1310cc530c60459608246550eb977a1ae27b6ce4"
|
||||
integrity sha512-P4gc1WdNiX89FZIAhIvR4Bj3sdL7VIpoM80L6otnoeLCZhyFfEOXHGAElvO2z7BuBqJBp1f3pdVDrBQNE6bhyw==
|
||||
dependencies:
|
||||
"@radix-ui/react-icons" "^1.3.0"
|
||||
"@radix-ui/react-slot" "^1.1.0"
|
||||
class-variance-authority "^0.7.0"
|
||||
clsx "^2.1.1"
|
||||
lucide-react "^0.445.0"
|
||||
next-themes "^0.4.6"
|
||||
sonner "^2.0.7"
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@signozhq/table@0.3.4":
|
||||
version "0.3.4"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/table/-/table-0.3.4.tgz#5f243f2977f21fe351207d4ae6db390e400a7933"
|
||||
integrity sha512-5B3kxYrfNEE9TH1orxAl6CNlESnyNVOgEW08ji9u2kz+khUd0euMCnZPrTBbotRW/INoEwRQxyBPj3auaa3jjQ==
|
||||
dependencies:
|
||||
"@radix-ui/react-icons" "^1.3.0"
|
||||
"@radix-ui/react-slot" "^1.1.0"
|
||||
"@tanstack/react-table" "^8.21.3"
|
||||
"@tanstack/react-virtual" "^3.13.9"
|
||||
"@types/lodash-es" "^4.17.12"
|
||||
class-variance-authority "^0.7.0"
|
||||
clsx "^2.1.1"
|
||||
lodash-es "^4.17.21"
|
||||
lucide-react "^0.445.0"
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@sinclair/typebox@^0.25.16":
|
||||
version "0.25.24"
|
||||
resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz"
|
||||
@@ -4359,13 +4332,6 @@
|
||||
dependencies:
|
||||
"@tanstack/table-core" "8.20.5"
|
||||
|
||||
"@tanstack/react-table@^8.21.3":
|
||||
version "8.21.3"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.21.3.tgz#2c38c747a5731c1a07174fda764b9c2b1fb5e91b"
|
||||
integrity sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==
|
||||
dependencies:
|
||||
"@tanstack/table-core" "8.21.3"
|
||||
|
||||
"@tanstack/react-virtual@3.11.2":
|
||||
version "3.11.2"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.11.2.tgz#d6b9bd999c181f0a2edce270c87a2febead04322"
|
||||
@@ -4373,33 +4339,16 @@
|
||||
dependencies:
|
||||
"@tanstack/virtual-core" "3.11.2"
|
||||
|
||||
"@tanstack/react-virtual@^3.13.9":
|
||||
version "3.13.12"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz#d372dc2783739cc04ec1a728ca8203937687a819"
|
||||
integrity sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==
|
||||
dependencies:
|
||||
"@tanstack/virtual-core" "3.13.12"
|
||||
|
||||
"@tanstack/table-core@8.20.5":
|
||||
version "8.20.5"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.20.5.tgz#3974f0b090bed11243d4107283824167a395cf1d"
|
||||
integrity sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==
|
||||
|
||||
"@tanstack/table-core@8.21.3":
|
||||
version "8.21.3"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.21.3.tgz#2977727d8fc8dfa079112d9f4d4c019110f1732c"
|
||||
integrity sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==
|
||||
|
||||
"@tanstack/virtual-core@3.11.2":
|
||||
version "3.11.2"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.11.2.tgz#00409e743ac4eea9afe5b7708594d5fcebb00212"
|
||||
integrity sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw==
|
||||
|
||||
"@tanstack/virtual-core@3.13.12":
|
||||
version "3.13.12"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz#1dff176df9cc8f93c78c5e46bcea11079b397578"
|
||||
integrity sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==
|
||||
|
||||
"@testing-library/dom@^8.5.0":
|
||||
version "8.20.0"
|
||||
resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.0.tgz"
|
||||
@@ -4487,6 +4436,11 @@
|
||||
resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz"
|
||||
integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==
|
||||
|
||||
"@tsconfig/svelte@^1.0.0":
|
||||
version "1.0.13"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/svelte/-/svelte-1.0.13.tgz#2fa34376627192c0d643ce54964915e2bd3a58e4"
|
||||
integrity sha512-5lYJP45Xllo4yE/RUBccBT32eBlRDbqN8r1/MIvQbKxW3aFqaYPCNgm8D5V20X4ShHcwvYWNlKg3liDh1MlBoA==
|
||||
|
||||
"@tweenjs/tween.js@18 - 19", "@tweenjs/tween.js@19":
|
||||
version "19.0.0"
|
||||
resolved "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-19.0.0.tgz"
|
||||
@@ -4617,6 +4571,11 @@
|
||||
tapable "^2.0.0"
|
||||
webpack "^5.1.0"
|
||||
|
||||
"@types/css-font-loading-module@0.0.7":
|
||||
version "0.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz#2f98ede46acc0975de85c0b7b0ebe06041d24601"
|
||||
integrity sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==
|
||||
|
||||
"@types/d3-array@3.0.3":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.0.3.tgz#87d990bf504d14ad6b16766979d04e943c046dac"
|
||||
@@ -4881,13 +4840,6 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/lodash-es@^4.17.12":
|
||||
version "4.17.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash-es/-/lodash-es-4.17.12.tgz#65f6d1e5f80539aa7cfbfc962de5def0cf4f341b"
|
||||
integrity sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==
|
||||
dependencies:
|
||||
"@types/lodash" "*"
|
||||
|
||||
"@types/lodash-es@^4.17.4":
|
||||
version "4.17.7"
|
||||
resolved "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.7.tgz"
|
||||
@@ -5787,6 +5739,11 @@
|
||||
resolved "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz"
|
||||
integrity sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==
|
||||
|
||||
"@xstate/fsm@^1.4.0":
|
||||
version "1.6.5"
|
||||
resolved "https://registry.yarnpkg.com/@xstate/fsm/-/fsm-1.6.5.tgz#f599e301997ad7e3c572a0b1ff0696898081bea5"
|
||||
integrity sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==
|
||||
|
||||
"@xstate/react@^3.0.0":
|
||||
version "3.2.2"
|
||||
resolved "https://registry.npmjs.org/@xstate/react/-/react-3.2.2.tgz"
|
||||
@@ -6761,6 +6718,11 @@ balanced-match@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
|
||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||
|
||||
base64-arraybuffer@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc"
|
||||
integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==
|
||||
|
||||
base64-js@^1.3.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz"
|
||||
@@ -9460,7 +9422,7 @@ fb-watchman@^2.0.0:
|
||||
dependencies:
|
||||
bser "2.1.1"
|
||||
|
||||
fflate@^0.4.8:
|
||||
fflate@^0.4.4, fflate@^0.4.8:
|
||||
version "0.4.8"
|
||||
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae"
|
||||
integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==
|
||||
@@ -13218,6 +13180,11 @@ minipass@^4.2.4:
|
||||
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c"
|
||||
integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==
|
||||
|
||||
mitt@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1"
|
||||
integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==
|
||||
|
||||
mkdirp@^0.5.6:
|
||||
version "0.5.6"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
|
||||
@@ -13389,11 +13356,6 @@ new-array@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/new-array/-/new-array-1.0.0.tgz"
|
||||
integrity sha512-K5AyFYbuHZ4e/ti52y7k18q8UHsS78FlRd85w2Fmsd6AkuLipDihPflKC0p3PN5i8II7+uHxo+CtkLiJDfmS5A==
|
||||
|
||||
next-themes@^0.4.6:
|
||||
version "0.4.6"
|
||||
resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.4.6.tgz#8d7e92d03b8fea6582892a50a928c9b23502e8b6"
|
||||
integrity sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==
|
||||
|
||||
ngraph.events@^1.0.0, ngraph.events@^1.2.1:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.npmjs.org/ngraph.events/-/ngraph.events-1.2.2.tgz"
|
||||
@@ -16057,6 +16019,40 @@ robust-predicates@^3.0.2:
|
||||
resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771"
|
||||
integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==
|
||||
|
||||
rrdom@^0.1.7:
|
||||
version "0.1.7"
|
||||
resolved "https://registry.yarnpkg.com/rrdom/-/rrdom-0.1.7.tgz#f2f49bfd01b59291bb7b0d981371a5e02a18e2aa"
|
||||
integrity sha512-ZLd8f14z9pUy2Hk9y636cNv5Y2BMnNEY99wxzW9tD2BLDfe1xFxtLjB4q/xCBYo6HRe0wofzKzjm4JojmpBfFw==
|
||||
dependencies:
|
||||
rrweb-snapshot "^2.0.0-alpha.4"
|
||||
|
||||
rrweb-player@1.0.0-alpha.4:
|
||||
version "1.0.0-alpha.4"
|
||||
resolved "https://registry.yarnpkg.com/rrweb-player/-/rrweb-player-1.0.0-alpha.4.tgz#57576343aaff6c6fb266689fd5d63092be46967c"
|
||||
integrity sha512-Wlmn9GZ5Fdqa37vd3TzsYdLl/JWEvXNUrLCrYpnOwEgmY409HwVIvvA5aIo7k582LoKgdRCsB87N+f0oWAR0Kg==
|
||||
dependencies:
|
||||
"@tsconfig/svelte" "^1.0.0"
|
||||
rrweb "^2.0.0-alpha.4"
|
||||
|
||||
rrweb-snapshot@^2.0.0-alpha.4:
|
||||
version "2.0.0-alpha.4"
|
||||
resolved "https://registry.yarnpkg.com/rrweb-snapshot/-/rrweb-snapshot-2.0.0-alpha.4.tgz#2801bf5946177b9d685a01661a62d9d2e958f174"
|
||||
integrity sha512-KQ2OtPpXO5jLYqg1OnXS/Hf+EzqnZyP5A+XPqBCjYpj3XIje/Od4gdUwjbFo3cVuWq5Cw5Y1d3/xwgIS7/XpQQ==
|
||||
|
||||
rrweb@^2.0.0-alpha.4:
|
||||
version "2.0.0-alpha.4"
|
||||
resolved "https://registry.yarnpkg.com/rrweb/-/rrweb-2.0.0-alpha.4.tgz#3c7cf2f1bcf44f7a88dd3fad00ee8d6dd711f258"
|
||||
integrity sha512-wEHUILbxDPcNwkM3m4qgPgXAiBJyqCbbOHyVoNEVBJzHszWEFYyTbrZqUdeb1EfmTRC2PsumCIkVcomJ/xcOzA==
|
||||
dependencies:
|
||||
"@rrweb/types" "^2.0.0-alpha.4"
|
||||
"@types/css-font-loading-module" "0.0.7"
|
||||
"@xstate/fsm" "^1.4.0"
|
||||
base64-arraybuffer "^1.0.1"
|
||||
fflate "^0.4.4"
|
||||
mitt "^3.0.0"
|
||||
rrdom "^0.1.7"
|
||||
rrweb-snapshot "^2.0.0-alpha.4"
|
||||
|
||||
rtl-css-js@^1.14.0, rtl-css-js@^1.16.1:
|
||||
version "1.16.1"
|
||||
resolved "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz"
|
||||
@@ -16498,11 +16494,6 @@ sockjs@^0.3.24:
|
||||
uuid "^8.3.2"
|
||||
websocket-driver "^0.7.4"
|
||||
|
||||
sonner@^2.0.7:
|
||||
version "2.0.7"
|
||||
resolved "https://registry.yarnpkg.com/sonner/-/sonner-2.0.7.tgz#810c1487a67ec3370126e0f400dfb9edddc3e4f6"
|
||||
integrity sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==
|
||||
|
||||
sort-asc@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.npmjs.org/sort-asc/-/sort-asc-0.1.0.tgz"
|
||||
|
||||
@@ -402,16 +402,6 @@ func (b *traceQueryStatementBuilder) buildTraceQuery(
|
||||
innerSB.OrderBy("duration_nano DESC")
|
||||
innerSB.SQL("LIMIT 1 BY trace_id")
|
||||
|
||||
if query.Limit > 0 {
|
||||
innerSB.Limit(query.Limit)
|
||||
} else {
|
||||
innerSB.Limit(100)
|
||||
}
|
||||
|
||||
if query.Offset > 0 {
|
||||
innerSB.Offset(query.Offset)
|
||||
}
|
||||
|
||||
innerSQL, innerArgs := innerSB.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
cteFragments = append(cteFragments, fmt.Sprintf("__toe_duration_sorted AS (%s)", innerSQL))
|
||||
@@ -447,6 +437,10 @@ func (b *traceQueryStatementBuilder) buildTraceQuery(
|
||||
mainSB.Limit(100)
|
||||
}
|
||||
|
||||
if query.Offset > 0 {
|
||||
mainSB.Offset(query.Offset)
|
||||
}
|
||||
|
||||
mainSQL, mainArgs := mainSB.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
// combine it all together: WITH … SELECT …
|
||||
|
||||
@@ -547,25 +547,8 @@ func TestStatementBuilderTraceQuery(t *testing.T) {
|
||||
Limit: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __toe AS (SELECT trace_id FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __toe_duration_sorted AS (SELECT trace_id, duration_nano, resource_string_service$$name as `service.name`, name FROM signoz_traces.distributed_signoz_index_v3 WHERE parent_span_id = '' AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY duration_nano DESC LIMIT 1 BY trace_id LIMIT ?) SELECT __toe_duration_sorted.`service.name` AS `service.name`, __toe_duration_sorted.name AS `name`, count() AS span_count, __toe_duration_sorted.duration_nano AS `duration_nano`, __toe_duration_sorted.trace_id AS `trace_id` FROM __toe INNER JOIN __toe_duration_sorted ON __toe.trace_id = __toe_duration_sorted.trace_id GROUP BY trace_id, duration_nano, name, `service.name` ORDER BY duration_nano DESC LIMIT 1 BY trace_id LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "List query with mat selected fields with offset",
|
||||
requestType: qbtypes.RequestTypeTrace,
|
||||
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "service.name = 'redis-manual'",
|
||||
},
|
||||
Limit: 10,
|
||||
Offset: 10,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __toe AS (SELECT trace_id FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __toe_duration_sorted AS (SELECT trace_id, duration_nano, resource_string_service$$name as `service.name`, name FROM signoz_traces.distributed_signoz_index_v3 WHERE parent_span_id = '' AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY duration_nano DESC LIMIT 1 BY trace_id LIMIT ? OFFSET ?) SELECT __toe_duration_sorted.`service.name` AS `service.name`, __toe_duration_sorted.name AS `name`, count() AS span_count, __toe_duration_sorted.duration_nano AS `duration_nano`, __toe_duration_sorted.trace_id AS `trace_id` FROM __toe INNER JOIN __toe_duration_sorted ON __toe.trace_id = __toe_duration_sorted.trace_id GROUP BY trace_id, duration_nano, name, `service.name` ORDER BY duration_nano DESC LIMIT 1 BY trace_id LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, 10, 10},
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __toe AS (SELECT trace_id FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __toe_duration_sorted AS (SELECT trace_id, duration_nano, resource_string_service$$name as `service.name`, name FROM signoz_traces.distributed_signoz_index_v3 WHERE parent_span_id = '' AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY duration_nano DESC LIMIT 1 BY trace_id) SELECT __toe_duration_sorted.`service.name` AS `service.name`, __toe_duration_sorted.name AS `name`, count() AS span_count, __toe_duration_sorted.duration_nano AS `duration_nano`, __toe_duration_sorted.trace_id AS `trace_id` FROM __toe INNER JOIN __toe_duration_sorted ON __toe.trace_id = __toe_duration_sorted.trace_id GROUP BY trace_id, duration_nano, name, `service.name` ORDER BY duration_nano DESC LIMIT 1 BY trace_id LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
||||
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user