Merge branch 'main' into improveTraceQuery

This commit is contained in:
Ekansh Gupta 2025-08-28 22:47:16 +05:30 committed by GitHub
commit 334b717f23
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 894 additions and 156 deletions

View File

@ -26,7 +26,7 @@ const config: Config.InitialOptions = {
'^.+\\.(js|jsx)$': 'babel-jest', '^.+\\.(js|jsx)$': 'babel-jest',
}, },
transformIgnorePatterns: [ transformIgnorePatterns: [
'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)/)', '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)/)',
], ],
setupFilesAfterEnv: ['<rootDir>jest.setup.ts'], setupFilesAfterEnv: ['<rootDir>jest.setup.ts'],
testPathIgnorePatterns: ['/node_modules/', '/public/'], testPathIgnorePatterns: ['/node_modules/', '/public/'],

View File

@ -48,6 +48,8 @@
"@signozhq/design-tokens": "1.1.4", "@signozhq/design-tokens": "1.1.4",
"@signozhq/input": "0.0.2", "@signozhq/input": "0.0.2",
"@signozhq/popover": "0.0.0", "@signozhq/popover": "0.0.0",
"@signozhq/sonner": "0.1.0",
"@signozhq/table": "0.3.4",
"@tanstack/react-table": "8.20.6", "@tanstack/react-table": "8.20.6",
"@tanstack/react-virtual": "3.11.2", "@tanstack/react-virtual": "3.11.2",
"@uiw/codemirror-theme-copilot": "4.23.11", "@uiw/codemirror-theme-copilot": "4.23.11",

View File

@ -23,6 +23,7 @@ import {
} from 'container/LogDetailedView/utils'; } from 'container/LogDetailedView/utils';
import useInitialQuery from 'container/LogsExplorerContext/useInitialQuery'; import useInitialQuery from 'container/LogsExplorerContext/useInitialQuery';
import { useOptionsMenu } from 'container/OptionsMenu'; import { useOptionsMenu } from 'container/OptionsMenu';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
@ -94,6 +95,8 @@ function LogDetailInner({
const { notifications } = useNotifications(); const { notifications } = useNotifications();
const { onLogCopy } = useCopyLogLink(log?.id);
const LogJsonData = log ? aggregateAttributesResourcesToString(log) : ''; const LogJsonData = log ? aggregateAttributesResourcesToString(log) : '';
const handleModeChange = (e: RadioChangeEvent): void => { const handleModeChange = (e: RadioChangeEvent): void => {
@ -333,6 +336,14 @@ function LogDetailInner({
onClick={handleFilterVisible} 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> </div>
{isFilterVisible && contextQuery?.builder.queryData[0] && ( {isFilterVisible && contextQuery?.builder.queryData[0] && (
<div className="log-detail-drawer-query-container"> <div className="log-detail-drawer-query-container">

View File

@ -56,6 +56,8 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
.map(({ name }) => ({ .map(({ name }) => ({
title: name, title: name,
dataIndex: name, dataIndex: name,
accessorKey: name,
id: name.toLowerCase().replace(/\./g, '_'),
key: name, key: name,
render: (field): ColumnTypeRender<Record<string, unknown>> => ({ render: (field): ColumnTypeRender<Record<string, unknown>> => ({
props: { props: {
@ -83,7 +85,10 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
// We do not need any title and data index for the log state indicator // We do not need any title and data index for the log state indicator
title: '', title: '',
dataIndex: '', dataIndex: '',
// eslint-disable-next-line sonarjs/no-duplicate-string
key: 'state-indicator', key: 'state-indicator',
accessorKey: 'state-indicator',
id: 'state-indicator',
render: (_, item): ColumnTypeRender<Record<string, unknown>> => ({ render: (_, item): ColumnTypeRender<Record<string, unknown>> => ({
children: ( children: (
<div className={cx('state-indicator', fontSize)}> <div className={cx('state-indicator', fontSize)}>
@ -101,6 +106,8 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
title: 'timestamp', title: 'timestamp',
dataIndex: 'timestamp', dataIndex: 'timestamp',
key: 'timestamp', key: 'timestamp',
accessorKey: 'timestamp',
id: 'timestamp',
// https://github.com/ant-design/ant-design/discussions/36886 // https://github.com/ant-design/ant-design/discussions/36886
render: ( render: (
field: string | number, field: string | number,
@ -135,6 +142,8 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
title: 'body', title: 'body',
dataIndex: 'body', dataIndex: 'body',
key: 'body', key: 'body',
accessorKey: 'body',
id: 'body',
render: ( render: (
field: string | number, field: string | number,
): ColumnTypeRender<Record<string, unknown>> => ({ ): ColumnTypeRender<Record<string, unknown>> => ({

View File

@ -33,4 +33,5 @@ export enum LOCALSTORAGE {
QUICK_FILTERS_SETTINGS_ANNOUNCEMENT = 'QUICK_FILTERS_SETTINGS_ANNOUNCEMENT', QUICK_FILTERS_SETTINGS_ANNOUNCEMENT = 'QUICK_FILTERS_SETTINGS_ANNOUNCEMENT',
FUNNEL_STEPS = 'FUNNEL_STEPS', FUNNEL_STEPS = 'FUNNEL_STEPS',
LAST_USED_CUSTOM_TIME_RANGES = 'LAST_USED_CUSTOM_TIME_RANGES', LAST_USED_CUSTOM_TIME_RANGES = 'LAST_USED_CUSTOM_TIME_RANGES',
SHOW_FREQUENCY_CHART = 'SHOW_FREQUENCY_CHART',
} }

View File

@ -4,6 +4,7 @@
import './AppLayout.styles.scss'; import './AppLayout.styles.scss';
import * as Sentry from '@sentry/react'; import * as Sentry from '@sentry/react';
import { Toaster } from '@signozhq/sonner';
import { Flex } from 'antd'; import { Flex } from 'antd';
import getLocalStorageApi from 'api/browser/localstorage/get'; import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set'; import setLocalStorageApi from 'api/browser/localstorage/set';
@ -852,6 +853,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
{showChangelogModal && changelog && ( {showChangelogModal && changelog && (
<ChangelogModal changelog={changelog} onClose={toggleChangelogModal} /> <ChangelogModal changelog={changelog} onClose={toggleChangelogModal} />
)} )}
<Toaster />
</Layout> </Layout>
); );
} }

View File

@ -1,6 +1,6 @@
.explorer-options-container { .explorer-options-container {
position: fixed; position: fixed;
bottom: 8px; bottom: 0px;
left: calc(50% + 240px); left: calc(50% + 240px);
transform: translate(calc(-50% - 120px), 0); transform: translate(calc(-50% - 120px), 0);
transition: left 0.2s linear; transition: left 0.2s linear;
@ -74,6 +74,7 @@
display: flex; display: flex;
gap: 16px; gap: 16px;
z-index: 1; z-index: 1;
.ant-select-selector { .ant-select-selector {
padding: 0 !important; padding: 0 !important;
} }

View File

@ -27,7 +27,7 @@
} }
.explorer-show-btn { .explorer-show-btn {
border-radius: 10px 10px 0px 0px; border-radius: 6px 6px 0px 0px;
border: 1px solid var(--bg-slate-400); border: 1px solid var(--bg-slate-400);
background: rgba(22, 24, 29, 0.4); background: rgba(22, 24, 29, 0.4);
box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.25); box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.25);

View File

@ -14,4 +14,7 @@ export const ContentWrapper = styled(Row)`
export const Wrapper = styled.div` export const Wrapper = styled.div`
padding-bottom: 4rem; padding-bottom: 4rem;
padding-top: 1rem;
padding-left: 1rem;
padding-right: 1rem;
`; `;

View File

@ -1,14 +0,0 @@
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';
}
`;

View File

@ -0,0 +1,25 @@
.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);
}
}

View File

@ -1,3 +1,5 @@
import './LogsExplorerChart.styles.scss';
import Graph from 'components/Graph'; import Graph from 'components/Graph';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import { QueryParams } from 'constants/query'; import { QueryParams } from 'constants/query';
@ -15,7 +17,6 @@ import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime';
import { LogsExplorerChartProps } from './LogsExplorerChart.interfaces'; import { LogsExplorerChartProps } from './LogsExplorerChart.interfaces';
import { CardStyled } from './LogsExplorerChart.styled';
import { getColorsForSeverityLabels } from './utils'; import { getColorsForSeverityLabels } from './utils';
function LogsExplorerChart({ function LogsExplorerChart({
@ -100,9 +101,11 @@ function LogsExplorerChart({
); );
return ( return (
<CardStyled className={className}> <div className={`${className} logs-frequency-chart-container`}>
{isLoading ? ( {isLoading ? (
<div className="logs-frequency-chart-loading">
<Spinner size="default" height="100%" /> <Spinner size="default" height="100%" />
</div>
) : ( ) : (
<Graph <Graph
name="logsExplorerChart" name="logsExplorerChart"
@ -115,7 +118,7 @@ function LogsExplorerChart({
maxTime={chartMaxTime} maxTime={chartMaxTime}
/> />
)} )}
</CardStyled> </div>
); );
} }

View File

@ -0,0 +1,240 @@
/* 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;

View File

@ -11,4 +11,5 @@ export type LogsExplorerListProps = {
isError: boolean; isError: boolean;
error?: Error | APIError; error?: Error | APIError;
isFilterApplied: boolean; isFilterApplied: boolean;
isFrequencyChartVisible: boolean;
}; };

View File

@ -9,4 +9,362 @@
letter-spacing: -0.005em; letter-spacing: -0.005em;
text-align: left; text-align: left;
min-height: 500px; 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);
}
}
}
}
} }

View File

@ -27,7 +27,7 @@ import { ILog } from 'types/api/logs/log';
import { DataSource, StringOperators } from 'types/common/queryBuilder'; import { DataSource, StringOperators } from 'types/common/queryBuilder';
import NoLogs from '../NoLogs/NoLogs'; import NoLogs from '../NoLogs/NoLogs';
import InfinityTableView from './InfinityTableView'; import ColumnView from './ColumnView/ColumnView';
import { LogsExplorerListProps } from './LogsExplorerList.interfaces'; import { LogsExplorerListProps } from './LogsExplorerList.interfaces';
import { InfinityWrapperStyled } from './styles'; import { InfinityWrapperStyled } from './styles';
import { import {
@ -48,6 +48,7 @@ function LogsExplorerList({
isError, isError,
error, error,
isFilterApplied, isFilterApplied,
isFrequencyChartVisible,
}: LogsExplorerListProps): JSX.Element { }: LogsExplorerListProps): JSX.Element {
const ref = useRef<VirtuosoHandle>(null); const ref = useRef<VirtuosoHandle>(null);
@ -90,6 +91,7 @@ function LogsExplorerList({
}); });
} }
}, [isLoading, isFetching, isError, logs.length]); }, [isLoading, isFetching, isError, logs.length]);
const getItemContent = useCallback( const getItemContent = useCallback(
(_: number, log: ILog): JSX.Element => { (_: number, log: ILog): JSX.Element => {
if (options.format === 'raw') { if (options.format === 'raw') {
@ -128,75 +130,6 @@ 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(() => { const isTraceToLogsNavigation = useMemo(() => {
if (!currentStagedQueryData) return false; if (!currentStagedQueryData) return false;
return isTraceToLogsQuery(currentStagedQueryData); return isTraceToLogsQuery(currentStagedQueryData);
@ -236,6 +169,83 @@ function LogsExplorerList({
return getEmptyLogsListConfig(handleClearFilters); return getEmptyLogsListConfig(handleClearFilters);
}, [isTraceToLogsNavigation, 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 ( return (
<div className="logs-list-view-container"> <div className="logs-list-view-container">
{(isLoading || (isFetching && logs.length === 0)) && <LogsLoading />} {(isLoading || (isFetching && logs.length === 0)) && <LogsLoading />}
@ -264,9 +274,7 @@ function LogsExplorerList({
{!isLoading && !isError && logs.length > 0 && ( {!isLoading && !isError && logs.length > 0 && (
<> <>
<InfinityWrapperStyled data-testid="logs-list-virtuoso">
{renderContent} {renderContent}
</InfinityWrapperStyled>
<LogDetail <LogDetail
selectedTab={VIEW_TYPES.OVERVIEW} selectedTab={VIEW_TYPES.OVERVIEW}

View File

@ -9,7 +9,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
padding-bottom: 60px; padding-bottom: 10px;
.views-tabs-container { .views-tabs-container {
padding: 8px 16px; padding: 8px 16px;
@ -216,7 +216,10 @@
background-color: var(--bg-ink-500); background-color: var(--bg-ink-500);
} }
.logs-histogram { .logs-frequency-chart-container {
padding: 0px 8px;
.logs-frequency-chart {
.ant-card-body { .ant-card-body {
height: 140px; height: 140px;
min-height: 140px; min-height: 140px;
@ -227,6 +230,7 @@
margin-bottom: 0px; margin-bottom: 0px;
} }
} }
}
.lightMode { .lightMode {
.logs-explorer-views-container { .logs-explorer-views-container {

View File

@ -2,6 +2,8 @@
import './LogsExplorerViews.styles.scss'; import './LogsExplorerViews.styles.scss';
import { Button, Switch, Typography } from 'antd'; 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 { getQueryStats, WsDataEvent } from 'api/common/getQueryStats';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig'; import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
@ -103,7 +105,13 @@ function LogsExplorerViewsContainer({
}): JSX.Element { }): JSX.Element {
const { safeNavigate } = useSafeNavigate(); const { safeNavigate } = useSafeNavigate();
const dispatch = useDispatch(); const dispatch = useDispatch();
const [showFrequencyChart, setShowFrequencyChart] = useState(true);
const [showFrequencyChart, setShowFrequencyChart] = useState(false);
useEffect(() => {
const frequencyChart = getFromLocalstorage(LOCALSTORAGE.SHOW_FREQUENCY_CHART);
setShowFrequencyChart(frequencyChart === 'true');
}, []);
// this is to respect the panel type present in the URL rather than defaulting it to list always. // this is to respect the panel type present in the URL rather than defaulting it to list always.
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST); const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
@ -672,6 +680,18 @@ function LogsExplorerViewsContainer({
[logs, timezone.value], [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 ( return (
<div className="logs-explorer-views-container"> <div className="logs-explorer-views-container">
<div className="logs-explorer-views-types"> <div className="logs-explorer-views-types">
@ -685,7 +705,7 @@ function LogsExplorerViewsContainer({
size="small" size="small"
checked={showFrequencyChart} checked={showFrequencyChart}
defaultChecked defaultChecked
onChange={(): void => setShowFrequencyChart(!showFrequencyChart)} onChange={handleToggleFrequencyChart}
/> />
</div> </div>
)} )}
@ -761,12 +781,14 @@ function LogsExplorerViewsContainer({
</div> </div>
{selectedPanelType === PANEL_TYPES.LIST && showFrequencyChart && ( {selectedPanelType === PANEL_TYPES.LIST && showFrequencyChart && (
<div className="logs-frequency-chart-container">
<LogsExplorerChart <LogsExplorerChart
className="logs-histogram" className="logs-frequency-chart"
isLoading={isFetchingListChartData || isLoadingListChartData} isLoading={isFetchingListChartData || isLoadingListChartData}
data={chartData} data={chartData}
isLogsExplorerViews={panelType === PANEL_TYPES.LIST} isLogsExplorerViews={panelType === PANEL_TYPES.LIST}
/> />
</div>
)} )}
<div className="logs-explorer-views-type-content"> <div className="logs-explorer-views-type-content">
@ -777,12 +799,12 @@ function LogsExplorerViewsContainer({
currentStagedQueryData={listQuery} currentStagedQueryData={listQuery}
logs={logs} logs={logs}
onEndReached={handleEndReached} onEndReached={handleEndReached}
isFrequencyChartVisible={showFrequencyChart}
isError={isError} isError={isError}
error={error as APIError} error={error as APIError}
isFilterApplied={!isEmpty(listQuery?.filters?.items)} isFilterApplied={!isEmpty(listQuery?.filters?.items)}
/> />
)} )}
{selectedPanelType === PANEL_TYPES.TIME_SERIES && ( {selectedPanelType === PANEL_TYPES.TIME_SERIES && (
<TimeSeriesView <TimeSeriesView
isLoading={isLoading || isFetching} isLoading={isLoading || isFetching}

View File

@ -65,17 +65,6 @@ const useOptionsMenu = ({
const [isFocused, setIsFocused] = useState<boolean>(false); const [isFocused, setIsFocused] = useState<boolean>(false);
const debouncedSearchText = useDebounce(searchText, 300); const debouncedSearchText = useDebounce(searchText, 300);
// const initialQueryParams = useMemo(
// () => ({
// searchText: '',
// aggregateAttribute: '',
// tagType: undefined,
// dataSource,
// aggregateOperator,
// }),
// [dataSource, aggregateOperator],
// );
const initialQueryParamsV5: QueryKeyRequestProps = useMemo( const initialQueryParamsV5: QueryKeyRequestProps = useMemo(
() => ({ () => ({
signal: dataSource, signal: dataSource,
@ -89,22 +78,6 @@ const useOptionsMenu = ({
redirectWithQuery: redirectWithOptionsData, redirectWithQuery: redirectWithOptionsData,
} = useUrlQueryData<OptionsQuery>(URL_OPTIONS, defaultOptionsQuery); } = 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( const initialQueriesV5 = useMemo(
() => () =>
initialOptions?.selectColumns?.map((column) => ({ initialOptions?.selectColumns?.map((column) => ({

View File

@ -42,11 +42,11 @@
gap: 8px; gap: 8px;
&.active-tab { &.active-tab {
background-color: var(--bg-ink-500); background-color: var(--bg-robin-500);
border-bottom: 1px solid var(--bg-ink-500); border-bottom: 1px solid var(--bg-robin-500);
&:hover { &:hover {
background-color: var(--bg-ink-500) !important; background-color: var(--bg-robin-500) !important;
} }
} }

View File

@ -12,6 +12,7 @@ export type UseCopyLogLink = {
isLogsExplorerPage: boolean; isLogsExplorerPage: boolean;
activeLogId: string | null; activeLogId: string | null;
onLogCopy: MouseEventHandler<HTMLElement>; onLogCopy: MouseEventHandler<HTMLElement>;
onClearActiveLog: () => void;
}; };
export type UseActiveLog = { export type UseActiveLog = {

View File

@ -1,6 +1,7 @@
import { toast } from '@signozhq/sonner';
import { QueryParams } from 'constants/query'; import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { useNotifications } from 'hooks/useNotifications'; import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
import useUrlQueryData from 'hooks/useUrlQueryData'; import useUrlQueryData from 'hooks/useUrlQueryData';
import { import {
@ -21,9 +22,10 @@ import { UseCopyLogLink } from './types';
export const useCopyLogLink = (logId?: string): UseCopyLogLink => { export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
const urlQuery = useUrlQuery(); const urlQuery = useUrlQuery();
const { pathname } = useLocation(); const { pathname, search } = useLocation();
const [, setCopy] = useCopyToClipboard(); const [, setCopy] = useCopyToClipboard();
const { notifications } = useNotifications();
const { safeNavigate } = useSafeNavigate();
const { queryData: activeLogId } = useUrlQueryData<string | null>( const { queryData: activeLogId } = useUrlQueryData<string | null>(
QueryParams.activeLogId, QueryParams.activeLogId,
@ -58,13 +60,19 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
const link = `${window.location.origin}${pathname}?${urlQuery.toString()}`; const link = `${window.location.origin}${pathname}?${urlQuery.toString()}`;
setCopy(link); setCopy(link);
notifications.success({
message: 'Copied to clipboard', toast.success('Copied to clipboard', { position: 'top-right' });
});
}, },
[logId, urlQuery, minTime, maxTime, pathname, setCopy, notifications], [logId, urlQuery, minTime, maxTime, pathname, setCopy],
); );
const onClearActiveLog = useCallback(() => {
const currentUrlQuery = new URLSearchParams(search);
currentUrlQuery.delete(QueryParams.activeLogId);
const newUrl = `${pathname}?${currentUrlQuery.toString()}`;
safeNavigate(newUrl);
}, [pathname, search, safeNavigate]);
useEffect(() => { useEffect(() => {
if (!isActiveLog) return; if (!isActiveLog) return;
@ -81,5 +89,6 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
isLogsExplorerPage, isLogsExplorerPage,
activeLogId, activeLogId,
onLogCopy, onLogCopy,
onClearActiveLog,
}; };
}; };

View File

@ -40,6 +40,13 @@ const useDragColumns = <T>(storageKey: LOCALSTORAGE): UseDragColumns<T> => {
[handleRedirectWithDraggedColumns], [handleRedirectWithDraggedColumns],
); );
const onColumnOrderChange = useCallback(
(newColumns: ColumnsType<T>): void => {
handleRedirectWithDraggedColumns(newColumns);
},
[handleRedirectWithDraggedColumns],
);
const redirectWithNewDraggedColumns = useCallback( const redirectWithNewDraggedColumns = useCallback(
async (localStorageColumns: string) => { async (localStorageColumns: string) => {
let nextDraggedColumns: ColumnsType<T> = []; let nextDraggedColumns: ColumnsType<T> = [];
@ -69,6 +76,7 @@ const useDragColumns = <T>(storageKey: LOCALSTORAGE): UseDragColumns<T> => {
return { return {
draggedColumns, draggedColumns,
onDragColumns, onDragColumns,
onColumnOrderChange,
}; };
}; };

View File

@ -7,4 +7,5 @@ export type UseDragColumns<T> = {
fromIndex: number, fromIndex: number,
toIndex: number, toIndex: number,
) => void; ) => void;
onColumnOrderChange: (newColumns: ColumnsType<T>) => void;
}; };

View File

@ -105,7 +105,6 @@ const logsQueryServerRequest = (): void =>
describe('Logs Explorer Tests', () => { describe('Logs Explorer Tests', () => {
test('Logs Explorer default view test without data', async () => { test('Logs Explorer default view test without data', async () => {
const { const {
getByText,
getByRole, getByRole,
queryByText, queryByText,
getByTestId, getByTestId,
@ -123,13 +122,10 @@ describe('Logs Explorer Tests', () => {
</MemoryRouter>, </MemoryRouter>,
); );
// check the presence of frequency chart content // by default is hidden, toggle the chart and check it's visibility
expect(getByText(frequencyChartContent)).toBeInTheDocument();
// toggle the chart and check it gets removed from the DOM
const histogramToggle = getByRole('switch'); const histogramToggle = getByRole('switch');
fireEvent.click(histogramToggle); fireEvent.click(histogramToggle);
expect(queryByText(frequencyChartContent)).not.toBeInTheDocument(); expect(queryByText(frequencyChartContent)).toBeInTheDocument();
// check the presence of search bar and query builder and absence of clickhouse // check the presence of search bar and query builder and absence of clickhouse
const searchView = getByTestId('search-view'); const searchView = getByTestId('search-view');
@ -277,10 +273,10 @@ describe('Logs Explorer Tests', () => {
const histogramToggle = getByRole('switch'); const histogramToggle = getByRole('switch');
expect(histogramToggle).toBeInTheDocument(); expect(histogramToggle).toBeInTheDocument();
expect(histogramToggle).toBeChecked(); expect(histogramToggle).toBeChecked();
expect(queryByText(frequencyChartContent)).toBeInTheDocument();
// toggle the chart and check it gets removed from the DOM // toggle the chart and check it gets removed from the DOM
await fireEvent.click(histogramToggle); await fireEvent.click(histogramToggle);
expect(histogramToggle).not.toBeChecked();
expect(queryByText(frequencyChartContent)).not.toBeInTheDocument(); expect(queryByText(frequencyChartContent)).not.toBeInTheDocument();
}); });
}); });

View File

@ -4284,6 +4284,38 @@
tailwind-merge "^2.5.2" tailwind-merge "^2.5.2"
tailwindcss-animate "^1.0.7" 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": "@sinclair/typebox@^0.25.16":
version "0.25.24" version "0.25.24"
resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz" resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz"
@ -4327,6 +4359,13 @@
dependencies: dependencies:
"@tanstack/table-core" "8.20.5" "@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": "@tanstack/react-virtual@3.11.2":
version "3.11.2" version "3.11.2"
resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.11.2.tgz#d6b9bd999c181f0a2edce270c87a2febead04322" resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.11.2.tgz#d6b9bd999c181f0a2edce270c87a2febead04322"
@ -4334,16 +4373,33 @@
dependencies: dependencies:
"@tanstack/virtual-core" "3.11.2" "@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": "@tanstack/table-core@8.20.5":
version "8.20.5" version "8.20.5"
resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.20.5.tgz#3974f0b090bed11243d4107283824167a395cf1d" resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.20.5.tgz#3974f0b090bed11243d4107283824167a395cf1d"
integrity sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg== 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": "@tanstack/virtual-core@3.11.2":
version "3.11.2" version "3.11.2"
resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.11.2.tgz#00409e743ac4eea9afe5b7708594d5fcebb00212" resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.11.2.tgz#00409e743ac4eea9afe5b7708594d5fcebb00212"
integrity sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw== 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": "@testing-library/dom@^8.5.0":
version "8.20.0" version "8.20.0"
resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.0.tgz" resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.0.tgz"
@ -4825,6 +4881,13 @@
dependencies: dependencies:
"@types/node" "*" "@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": "@types/lodash-es@^4.17.4":
version "4.17.7" version "4.17.7"
resolved "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.7.tgz" resolved "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.7.tgz"
@ -13326,6 +13389,11 @@ new-array@^1.0.0:
resolved "https://registry.npmjs.org/new-array/-/new-array-1.0.0.tgz" resolved "https://registry.npmjs.org/new-array/-/new-array-1.0.0.tgz"
integrity sha512-K5AyFYbuHZ4e/ti52y7k18q8UHsS78FlRd85w2Fmsd6AkuLipDihPflKC0p3PN5i8II7+uHxo+CtkLiJDfmS5A== 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: ngraph.events@^1.0.0, ngraph.events@^1.2.1:
version "1.2.2" version "1.2.2"
resolved "https://registry.npmjs.org/ngraph.events/-/ngraph.events-1.2.2.tgz" resolved "https://registry.npmjs.org/ngraph.events/-/ngraph.events-1.2.2.tgz"
@ -16430,6 +16498,11 @@ sockjs@^0.3.24:
uuid "^8.3.2" uuid "^8.3.2"
websocket-driver "^0.7.4" 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: sort-asc@^0.1.0:
version "0.1.0" version "0.1.0"
resolved "https://registry.npmjs.org/sort-asc/-/sort-asc-0.1.0.tgz" resolved "https://registry.npmjs.org/sort-asc/-/sort-asc-0.1.0.tgz"