Compare commits

...

27 Commits

Author SHA1 Message Date
Rajat Dabade
c3608111de [Refactor]: UI Updates (#4418)
* feat: save view for new design (#4392)

* feat: save view for new design

* refactor: done with save view

* refactor: time selector and stage and run query in same row

* refactor: remove old add to dashboard and setup alerts buttons

* refactor: clear save and remove tooltip in save view
2024-01-23 12:01:49 +05:30
Yunus M
e0c2000e00 feat: update styles for logs detail view 2024-01-22 19:55:22 +05:30
Yunus M
28b97d3b5a feat: update styles for logs detail view 2024-01-22 17:29:41 +05:30
Rajat Dabade
bbbacdeb6d [Refactor]: New design for Log details page (#4362)
New design for Log details page 

Co-authored-by: Vikrant Gupta <vikrant.thomso@gmail.com>
Co-authored-by: Yunus M <myounis.ar@live.com>
2024-01-19 13:24:06 +05:30
Yunus M
4bcab49f1f Settings theme change (#4368)
* feat: settings theme change
2024-01-19 12:34:06 +05:30
Vikrant Gupta
dc48d46e26 chore: styles improvement across new design (#4389)
* fix: improve date time styles

* feat: table view changes according to new design

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

* feat: change the tabs to new design buttons for query builder
2024-01-19 12:11:57 +05:30
Vikrant Gupta
aa63fd541b feat: handle new typing changes for date time picker v2 (#4386)
Co-authored-by: Yunus M <myounis.ar@live.com>
2024-01-17 19:44:54 +05:30
Vikrant Gupta
9a40cb72cf Merge pull request #4378 from SigNoz/fix-virtuoso-scroll
fix: virtuoso scroll refresh issue
2024-01-16 15:11:09 +05:30
Vikrant Gupta
a128b0635a fix: virtuoso scroll refresh issue 2024-01-16 14:57:15 +05:30
Vikrant Gupta
e6495532bc Merge pull request #4372 from SigNoz/handle-qb-design 2024-01-12 21:25:09 +05:30
Vikrant Gupta
194b15db79 feat: handle light theme 2024-01-12 18:28:04 +05:30
Vikrant Gupta
d4bea0df35 feat: handle light theme 2024-01-12 17:25:12 +05:30
Vikrant Gupta
f4234ccf46 feat: handle qb design changes across the application 2024-01-12 17:14:56 +05:30
Vikrant Gupta
89f6d08316 Merge pull request #4370 from SigNoz/consume-datetime
feat: integrate date time selector across app
2024-01-12 15:02:14 +05:30
Vikrant Gupta
5f9329c8d1 fix: remove dangling border after element removal 2024-01-12 12:30:41 +05:30
Vikrant Gupta
07ce1f520d feat: integrate date time selector across app 2024-01-12 12:23:48 +05:30
Vikrant Gupta
735e55f506 feat: date time custom time modal to render inside the new popover (#4366)
* feat: single calender for range picker

* fix: edgecases
2024-01-11 22:05:59 +05:30
Vikrant Gupta
023fb93b75 feat: handle light theme for logs explorer design changes (#4363)
* feat: handle light theme for list tables and dateTime selection

* feat: handle light theme for popover

* fix: address review comments
2024-01-11 14:36:59 +05:30
Vikrant Gupta
0ddeceb2ab fix: type errors (#4360) 2024-01-11 01:57:39 +05:30
Vikrant Gupta
a772d4cf54 fix: eslint error 2024-01-11 01:14:16 +05:30
Vikrant Gupta
7c97be51d6 feat: new table view for logs explorer list section (#4353)
* feat: table view changes for logs list

* feat: code refactor to support log line actions

* feat: code refactor to support log line actions

* fix: the positioning of the btns

* feat: fix the table onclick

* fix: header issue

* fix: on hover

* fix: type issue
2024-01-11 01:07:38 +05:30
Yunus M
beb63c0ce5 fix: lint errors 2024-01-11 00:39:10 +05:30
Yunus M
45fe4b1779 Query builder design update (#4359)
* feat: QB design update

* fix: add functionality and light mode styles

* fix: ts issues

* fix: update all css color variables to correct names
2024-01-11 00:29:53 +05:30
Vikrant Gupta
a025ce54d2 feat: logs list view changes (#4348)
* feat: logs list view changes

* fix: list view and toolbar styles

* feat: side btns

* feat: added auto refresh handler

* feat: handle popover close for btn click date time

* feat: extract the common log actions btn component

* feat: update the button for log line actions

* fix: event propagation from context button

* feat: use styles from ui-library
2024-01-10 14:39:32 +05:30
Vikrant Gupta
e7c8eae7c4 feat: added new toolbar for logs explorer (#4336) 2024-01-09 01:12:56 +05:30
Yunus M
6ff7d9dfb4 feat: update styles 2024-01-05 14:56:33 +05:30
Yunus M
abce08c851 feat: logs explorer - new design 2024-01-05 11:30:45 +05:30
153 changed files with 7656 additions and 1232 deletions

View File

@@ -36,7 +36,9 @@
"@mdx-js/loader": "2.3.0",
"@mdx-js/react": "2.3.0",
"@monaco-editor/react": "^4.3.1",
"@signozhq/design-tokens": "0.0.6",
"@radix-ui/react-tabs": "1.0.4",
"@radix-ui/react-tooltip": "1.0.7",
"@signozhq/design-tokens": "0.0.8",
"@uiw/react-md-editor": "3.23.5",
"@xstate/react": "^3.0.0",
"ansi-to-html": "0.7.2",

View File

@@ -0,0 +1,19 @@
<svg width="32" height="33" viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.36806 25.9481C5.93935 25.9481 3.15283 21.7098 3.15283 16.5002C3.15283 11.2907 5.94157 7.05238 9.36806 7.05238C12.7945 7.05238 15.5833 11.2907 15.5833 16.5002C15.5833 21.7098 12.7945 25.9481 9.36806 25.9481Z" fill="#FAFAFA"/>
<path d="M9.36815 7.49694C10.8414 7.49694 12.2524 8.38594 13.3391 10.0017C14.499 11.7241 15.139 14.0333 15.139 16.5003C15.139 18.9673 14.499 21.2764 13.3391 22.9989C12.2524 24.6146 10.8414 25.5036 9.36815 25.5036C7.89489 25.5036 6.48385 24.6146 5.39724 22.9989C4.23508 21.2764 3.59734 18.9673 3.59734 16.5003C3.59734 14.0333 4.23731 11.7241 5.39724 10.0017C6.48385 8.38594 7.89267 7.49694 9.36815 7.49694ZM9.36815 6.60794C5.69056 6.60794 2.7085 11.0374 2.7085 16.5003C2.7085 21.9632 5.69056 26.3926 9.36815 26.3926C13.0457 26.3926 16.0278 21.9632 16.0278 16.5003C16.0278 11.0374 13.0457 6.60794 9.36815 6.60794Z" fill="#B0BEC5"/>
<path d="M7.47266 15.5762C6.87269 15.0118 7.00602 13.8919 7.77487 13.0741C7.81486 13.0319 7.85486 12.9919 7.89708 12.9541C7.55488 12.7608 7.17934 12.6519 6.78381 12.6519C5.18611 12.6519 3.89062 14.414 3.89062 16.585C3.89062 18.756 5.18611 20.5182 6.78381 20.5182C8.3815 20.5182 9.67699 18.756 9.67699 16.585C9.67699 16.1962 9.63477 15.8184 9.55699 15.4629C8.83703 15.9806 7.97708 16.0495 7.47266 15.5762Z" fill="url(#paint0_linear_2122_5062)"/>
<path d="M22.6294 26.3932C26.3074 26.3932 29.289 21.9642 29.289 16.5008C29.289 11.0374 26.3074 6.60847 22.6294 6.60847C18.9514 6.60847 15.9697 11.0374 15.9697 16.5008C15.9697 21.9642 18.9514 26.3932 22.6294 26.3932Z" fill="#EEEEEE"/>
<path d="M22.6283 25.9493C19.2018 25.9493 16.4131 21.711 16.4131 16.5014C16.4131 11.2919 19.2018 7.05357 22.6283 7.05357C26.0548 7.05357 28.8435 11.2919 28.8435 16.5014C28.8435 21.711 26.057 25.9493 22.6283 25.9493Z" fill="#FAFAFA"/>
<path d="M22.6284 7.49816C24.1017 7.49816 25.5127 8.38716 26.5993 10.0029C27.7592 11.7254 28.3992 14.0345 28.3992 16.5015C28.3992 18.9685 27.7592 21.2777 26.5993 23.0001C25.5127 24.6159 24.1017 25.5049 22.6284 25.5049C21.1551 25.5049 19.7441 24.6159 18.6575 23.0001C17.4976 21.2777 16.8576 18.9685 16.8576 16.5015C16.8576 14.0345 17.4976 11.7254 18.6575 10.0029C19.7441 8.38716 21.1551 7.49816 22.6284 7.49816ZM22.6284 6.60916C18.9508 6.60916 15.9688 11.0386 15.9688 16.5015C15.9688 21.9644 18.9508 26.3939 22.6284 26.3939C26.306 26.3939 29.2881 21.9644 29.2881 16.5015C29.2881 11.0386 26.306 6.60916 22.6284 6.60916Z" fill="#B0BEC5"/>
<path d="M20.7339 15.5767C20.1339 15.0123 20.2672 13.8924 21.0361 13.0746C21.0761 13.0324 21.1161 12.9924 21.1583 12.9546C20.8161 12.7613 20.4406 12.6524 20.045 12.6524C18.4473 12.6524 17.1519 14.4146 17.1519 16.5856C17.1519 18.7566 18.4473 20.5187 20.045 20.5187C21.6427 20.5187 22.9382 18.7566 22.9382 16.5856C22.9382 16.1967 22.896 15.8189 22.8182 15.4634C22.1005 15.9812 21.2383 16.05 20.7339 15.5767Z" fill="url(#paint1_linear_2122_5062)"/>
<defs>
<linearGradient id="paint0_linear_2122_5062" x1="6.78232" y1="12.651" x2="6.78232" y2="20.5188" gradientUnits="userSpaceOnUse">
<stop stop-color="#424242"/>
<stop offset="1" stop-color="#212121"/>
</linearGradient>
<linearGradient id="paint1_linear_2122_5062" x1="20.0449" y1="12.6515" x2="20.0449" y2="20.5193" gradientUnits="userSpaceOnUse">
<stop stop-color="#424242"/>
<stop offset="1" stop-color="#212121"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -0,0 +1,91 @@
.custom-time-picker {
display: flex;
flex-direction: column;
}
.time-options-container {
.time-options-item {
margin: 2px 0;
padding: 8px;
border-radius: 2px;
&.active {
background-color: rgba($color: #000000, $alpha: 0.2);
&:hover {
cursor: pointer;
background-color: rgba($color: #000000, $alpha: 0.3);
}
}
&:hover {
cursor: pointer;
background-color: rgba($color: #000000, $alpha: 0.3);
}
}
}
.time-selection-dropdown-content {
min-width: 172px;
width: 100%;
}
.timeSelection-input {
display: flex;
gap: 8px;
height: 33px;
align-items: center;
padding: 4px 8px;
padding-left: 0px !important;
input::placeholder {
color: white;
}
input:focus::placeholder {
color: rgba($color: #ffffff, $alpha: 0.4);
}
}
.valid-format-error {
margin-top: 4px;
color: var(--bg-cherry-400) !important;
font-size: 13px !important;
font-weight: 400 !important;
}
.lightMode {
.time-options-container {
.time-options-item {
&.active {
background-color: rgba($color: #ffffff, $alpha: 0.2);
&:hover {
cursor: pointer;
background-color: rgba($color: #ffffff, $alpha: 0.3);
}
}
&:hover {
cursor: pointer;
background-color: rgba($color: #ffffff, $alpha: 0.3);
}
}
}
.timeSelection-input {
display: flex;
gap: 8px;
align-items: center;
padding: 4px 8px;
padding-left: 0px !important;
input::placeholder {
color: var(---bg-ink-300);
}
input:focus::placeholder {
color: rgba($color: #000000, $alpha: 0.4);
}
}
}

View File

@@ -0,0 +1,310 @@
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable jsx-a11y/no-static-element-interactions */
import './CustomTimePicker.styles.scss';
import { Input, Popover, Tooltip, Typography } from 'antd';
import cx from 'classnames';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
import { Options } from 'container/TopNav/DateTimeSelection/config';
import {
FixedDurationSuggestionOptions,
RelativeDurationSuggestionOptions,
} from 'container/TopNav/DateTimeSelectionV2/config';
import dayjs from 'dayjs';
import { defaultTo, noop } from 'lodash-es';
import debounce from 'lodash-es/debounce';
import { CheckCircle, ChevronDown, Clock } from 'lucide-react';
import {
ChangeEvent,
Dispatch,
SetStateAction,
useEffect,
useState,
} from 'react';
import { popupContainer } from 'utils/selectPopupContainer';
import CustomTimePickerPopoverContent from './CustomTimePickerPopoverContent';
const maxAllowedMinTimeInMonths = 6;
interface CustomTimePickerProps {
onSelect: (value: string) => void;
onError: (value: boolean) => void;
selectedValue: string;
selectedTime: string;
onValidCustomDateChange: ([t1, t2]: any[]) => void;
open: boolean;
setOpen: Dispatch<SetStateAction<boolean>>;
items: any[];
newPopover?: boolean;
customDateTimeVisible?: boolean;
setCustomDTPickerVisible?: Dispatch<SetStateAction<boolean>>;
onCustomDateHandler?: (dateTimeRange: DateTimeRangeType) => void;
handleGoLive?: () => void;
}
function CustomTimePicker({
onSelect,
onError,
items,
selectedValue,
selectedTime,
open,
setOpen,
onValidCustomDateChange,
newPopover,
customDateTimeVisible,
setCustomDTPickerVisible,
onCustomDateHandler,
handleGoLive,
}: CustomTimePickerProps): JSX.Element {
const [
selectedTimePlaceholderValue,
setSelectedTimePlaceholderValue,
] = useState('Select / Enter Time Range');
const [inputValue, setInputValue] = useState('');
const [inputStatus, setInputStatus] = useState<'' | 'error' | 'success'>('');
const [inputErrorMessage, setInputErrorMessage] = useState<string | null>(
null,
);
const [isInputFocused, setIsInputFocused] = useState(false);
const getSelectedTimeRangeLabel = (
selectedTime: string,
selectedTimeValue: string,
): string => {
if (selectedTime === 'custom') {
return selectedTimeValue;
}
for (let index = 0; index < Options.length; index++) {
if (Options[index].value === selectedTime) {
return Options[index].label;
}
}
for (
let index = 0;
index < RelativeDurationSuggestionOptions.length;
index++
) {
if (RelativeDurationSuggestionOptions[index].value === selectedTime) {
return RelativeDurationSuggestionOptions[index].label;
}
}
for (let index = 0; index < FixedDurationSuggestionOptions.length; index++) {
if (FixedDurationSuggestionOptions[index].value === selectedTime) {
return FixedDurationSuggestionOptions[index].label;
}
}
return '';
};
useEffect(() => {
const value = getSelectedTimeRangeLabel(selectedTime, selectedValue);
setSelectedTimePlaceholderValue(value);
}, [selectedTime, selectedValue]);
const hide = (): void => {
setOpen(false);
};
const handleOpenChange = (newOpen: boolean): void => {
setOpen(newOpen);
};
const debouncedHandleInputChange = debounce((inputValue): void => {
const isValidFormat = /^(\d+)([mhdw])$/.test(inputValue);
if (isValidFormat) {
setInputStatus('success');
onError(false);
setInputErrorMessage(null);
const match = inputValue.match(/^(\d+)([mhdw])$/);
const value = parseInt(match[1], 10);
const unit = match[2];
const currentTime = dayjs();
const maxAllowedMinTime = currentTime.subtract(
maxAllowedMinTimeInMonths,
'month',
);
let minTime = null;
switch (unit) {
case 'm':
minTime = currentTime.subtract(value, 'minute');
break;
case 'h':
minTime = currentTime.subtract(value, 'hour');
break;
case 'd':
minTime = currentTime.subtract(value, 'day');
break;
case 'w':
minTime = currentTime.subtract(value, 'week');
break;
default:
break;
}
if (minTime && minTime < maxAllowedMinTime) {
setInputStatus('error');
onError(true);
setInputErrorMessage('Please enter time less than 6 months');
} else {
onValidCustomDateChange([minTime, currentTime]);
}
} else {
setInputStatus('error');
onError(true);
setInputErrorMessage(null);
}
}, 300);
const handleInputChange = (event: ChangeEvent<HTMLInputElement>): void => {
const inputValue = event.target.value;
if (inputValue.length > 0) {
setOpen(false);
} else {
setOpen(true);
}
setInputValue(inputValue);
// Call the debounced function with the input value
debouncedHandleInputChange(inputValue);
};
const handleSelect = (label: string, value: string): void => {
onSelect(value);
setSelectedTimePlaceholderValue(label);
setInputStatus('');
onError(false);
setInputErrorMessage(null);
setInputValue('');
if (value !== 'custom') {
hide();
}
};
const content = (
<div className="time-selection-dropdown-content">
<div className="time-options-container">
{items?.map(({ value, label }) => (
<div
onClick={(): void => {
handleSelect(label, value);
}}
key={value}
className={cx(
'time-options-item',
selectedValue === value ? 'active' : '',
)}
>
{label}
</div>
))}
</div>
</div>
);
const handleFocus = (): void => {
setIsInputFocused(true);
};
const handleBlur = (): void => {
setIsInputFocused(false);
};
return (
<div className="custom-time-picker">
<Popover
className="timeSelection-input-container"
placement="bottomRight"
getPopupContainer={popupContainer}
rootClassName="date-time-root"
content={
newPopover ? (
<CustomTimePickerPopoverContent
setIsOpen={setOpen}
customDateTimeVisible={defaultTo(customDateTimeVisible, false)}
setCustomDTPickerVisible={defaultTo(setCustomDTPickerVisible, noop)}
onCustomDateHandler={defaultTo(onCustomDateHandler, noop)}
onSelectHandler={handleSelect}
handleGoLive={defaultTo(handleGoLive, noop)}
options={items}
/>
) : (
content
)
}
arrow={false}
trigger="hover"
open={open}
onOpenChange={handleOpenChange}
style={{
padding: 0,
}}
>
<Input
className="timeSelection-input"
type="text"
style={{
minWidth: '120px',
width: '100%',
}}
status={inputValue && inputStatus === 'error' ? 'error' : ''}
allowClear={!isInputFocused && selectedTime === 'custom'}
placeholder={
isInputFocused
? 'Time Format (1m or 2h or 3d or 4w)'
: selectedTimePlaceholderValue
}
value={inputValue}
onFocus={handleFocus}
onBlur={handleBlur}
onChange={handleInputChange}
prefix={
inputValue && inputStatus === 'success' ? (
<CheckCircle size={14} color="#51E7A8" />
) : (
<Tooltip title="Enter time in format (e.g., 1m, 2h, 3d, 4w)">
<Clock size={14} />
</Tooltip>
)
}
suffix={
<ChevronDown
size={14}
onClick={(): void => {
setOpen(!open);
}}
/>
}
/>
</Popover>
{inputStatus === 'error' && inputErrorMessage && (
<Typography.Title level={5} className="valid-format-error">
{inputErrorMessage}
</Typography.Title>
)}
</div>
);
}
export default CustomTimePicker;
CustomTimePicker.defaultProps = {
newPopover: false,
customDateTimeVisible: false,
setCustomDTPickerVisible: noop,
onCustomDateHandler: noop,
handleGoLive: noop,
};

View File

@@ -0,0 +1,121 @@
import { Button, DatePicker } from 'antd';
import ROUTES from 'constants/routes';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
import {
FixedDurationSuggestionOptions,
Option,
RelativeDurationSuggestionOptions,
} from 'container/TopNav/DateTimeSelectionV2/config';
import dayjs, { Dayjs } from 'dayjs';
import { Dispatch, SetStateAction, useMemo } from 'react';
import { useLocation } from 'react-router-dom';
interface CustomTimePickerPopoverContentProps {
options: any[];
setIsOpen: Dispatch<SetStateAction<boolean>>;
customDateTimeVisible: boolean;
setCustomDTPickerVisible: Dispatch<SetStateAction<boolean>>;
onCustomDateHandler: (dateTimeRange: DateTimeRangeType) => void;
onSelectHandler: (label: string, value: string) => void;
handleGoLive: () => void;
}
function CustomTimePickerPopoverContent({
options,
setIsOpen,
customDateTimeVisible,
setCustomDTPickerVisible,
onCustomDateHandler,
onSelectHandler,
handleGoLive,
}: CustomTimePickerPopoverContentProps): JSX.Element {
const { RangePicker } = DatePicker;
const { pathname } = useLocation();
const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [
pathname,
]);
const disabledDate = (current: Dayjs): boolean => {
const currentDay = dayjs(current);
return currentDay.isAfter(dayjs());
};
const onPopoverClose = (visible: boolean): void => {
if (!visible) {
setCustomDTPickerVisible(false);
}
setIsOpen(visible);
};
const onModalOkHandler = (date_time: any): void => {
if (date_time?.[1]) {
onPopoverClose(false);
}
onCustomDateHandler(date_time);
};
function getTimeChips(options: Option[]): JSX.Element {
return (
<div className="relative-date-time-section">
{options.map((option) => (
<Button
type="text"
className="time-btns"
key={option.label + option.value}
onClick={(): void => {
onSelectHandler(option.label, option.value);
}}
>
{option.label}
</Button>
))}
</div>
);
}
return (
<div className="date-time-popover">
<div className="date-time-options">
{isLogsExplorerPage && (
<Button className="data-time-live" type="text" onClick={handleGoLive}>
Live
</Button>
)}
{options.map((option) => (
<Button
type="text"
key={option.label + option.value}
onClick={(): void => {
onSelectHandler(option.label, option.value);
}}
className="date-time-options-btn"
>
{option.label}
</Button>
))}
</div>
<div className="relative-date-time">
{customDateTimeVisible ? (
<RangePicker
disabledDate={disabledDate}
allowClear
onCalendarChange={onModalOkHandler}
/>
) : (
<>
<div>
<div className="time-heading">RELATIVE TIMES</div>
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
</div>
<div>
<div className="time-heading">FIXED TIMES</div>
<div>{getTimeChips(FixedDurationSuggestionOptions)}</div>
</div>
</>
)}
</div>
</div>
);
}
export default CustomTimePickerPopoverContent;

View File

@@ -6,7 +6,6 @@ import {
} from '@ant-design/icons';
import {
Button,
Card,
Col,
Dropdown,
MenuProps,
@@ -152,95 +151,100 @@ function ExplorerCard({
const saveButtonType = isQueryUpdated ? 'default' : 'primary';
const saveButtonIcon = isQueryUpdated ? null : <SaveOutlined />;
const showSaveView = false;
return (
<>
<ExplorerCardHeadContainer size="small">
<Row align="middle">
<Col span={6}>
<Space>
<Typography>Query Builder</Typography>
<TextToolTip
url={ExploreHeaderToolTip.url}
text={ExploreHeaderToolTip.text}
useFilledIcon={false}
/>
</Space>
</Col>
<OffSetCol span={18}>
<Space size="large">
{viewsData?.data.data && viewsData?.data.data.length && (
<Space>
<Select
getPopupContainer={popupContainer}
loading={isLoading || isRefetching}
showSearch
placeholder="Select a view"
dropdownStyle={DropDownOverlay}
dropdownMatchSelectWidth={false}
optionLabelProp="value"
value={viewName || undefined}
{showSaveView && (
<ExplorerCardHeadContainer size="small">
<Row align="middle">
<Col span={6}>
<Space>
<Typography>Query Builder</Typography>
<TextToolTip
url={ExploreHeaderToolTip.url}
text={ExploreHeaderToolTip.text}
useFilledIcon={false}
/>
</Space>
</Col>
<OffSetCol span={18}>
<Space size="large">
{viewsData?.data.data && viewsData?.data.data.length && (
<Space>
<Select
getPopupContainer={popupContainer}
loading={isLoading || isRefetching}
showSearch
placeholder="Select a view"
dropdownStyle={DropDownOverlay}
dropdownMatchSelectWidth={false}
optionLabelProp="value"
value={viewName || undefined}
>
{viewsData?.data.data.map((view) => (
<Select.Option key={view.uuid} value={view.name}>
<MenuItemGenerator
viewName={view.name}
viewKey={viewKey}
createdBy={view.createdBy}
uuid={view.uuid}
refetchAllView={refetchAllView}
viewData={viewsData.data.data}
sourcePage={sourcepage}
/>
</Select.Option>
))}
</Select>
</Space>
)}
{isQueryUpdated && (
<Button
type="primary"
icon={<SaveOutlined />}
onClick={onUpdateQueryHandler}
>
{viewsData?.data.data.map((view) => (
<Select.Option key={view.uuid} value={view.name}>
<MenuItemGenerator
viewName={view.name}
viewKey={viewKey}
createdBy={view.createdBy}
uuid={view.uuid}
refetchAllView={refetchAllView}
viewData={viewsData.data.data}
sourcePage={sourcepage}
/>
</Select.Option>
))}
</Select>
</Space>
)}
{isQueryUpdated && (
<Button
type="primary"
icon={<SaveOutlined />}
onClick={onUpdateQueryHandler}
Save changes
</Button>
)}
<Popover
getPopupContainer={popupContainer}
placement="bottomLeft"
trigger="click"
content={
<SaveViewWithName
sourcePage={sourcepage}
handlePopOverClose={handleOpenChange}
refetchAllView={refetchAllView}
/>
}
showArrow={false}
open={isOpen}
onOpenChange={handleOpenChange}
>
Save changes
</Button>
)}
<Popover
getPopupContainer={popupContainer}
placement="bottomLeft"
trigger="click"
content={
<SaveViewWithName
sourcePage={sourcepage}
handlePopOverClose={handleOpenChange}
refetchAllView={refetchAllView}
/>
}
showArrow={false}
open={isOpen}
onOpenChange={handleOpenChange}
>
<Button
type={saveButtonType}
icon={saveButtonIcon}
data-testid="traces-save-view-action"
>
{isQueryUpdated
? SaveButtonText.SAVE_AS_NEW_VIEW
: SaveButtonText.SAVE_VIEW}
</Button>
</Popover>
<ShareAltOutlined onClick={onCopyUrlHandler} />
{viewKey && (
<Dropdown trigger={['click']} menu={moreOptionMenu}>
<MoreOutlined />
</Dropdown>
)}
</Space>
</OffSetCol>
</Row>
</ExplorerCardHeadContainer>
<Card>{children}</Card>
<Button
type={saveButtonType}
icon={saveButtonIcon}
data-testid="traces-save-view-action"
>
{isQueryUpdated
? SaveButtonText.SAVE_AS_NEW_VIEW
: SaveButtonText.SAVE_VIEW}
</Button>
</Popover>
<ShareAltOutlined onClick={onCopyUrlHandler} />
{viewKey && (
<Dropdown trigger={['click']} menu={moreOptionMenu}>
<MoreOutlined />
</Dropdown>
)}
</Space>
</OffSetCol>
</Row>
</ExplorerCardHeadContainer>
)}
<div>{children}</div>
</>
);
}

View File

@@ -3,6 +3,7 @@ import styled, { CSSProperties } from 'styled-components';
export const ExplorerCardHeadContainer = styled(Card)`
margin: 1rem 0;
padding: 0;
`;
export const OffSetCol = styled(Col)`

View File

@@ -3,8 +3,11 @@ import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
import { ActionItemProps } from 'container/LogDetailedView/ActionItem';
import { ILog } from 'types/api/logs/log';
import { VIEWS } from './constants';
export type LogDetailProps = {
log: ILog | null;
selectedTab: VIEWS;
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
Pick<ActionItemProps, 'onClickActionItem'> &
Partial<Pick<ActionItemProps, 'onClickActionItem'>> &
Pick<DrawerProps, 'onClose'>;

View File

@@ -0,0 +1,224 @@
.log-detail-drawer {
border-left: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
box-shadow: -4px 10px 16px 2px rgba(0, 0, 0, 0.2);
.ant-drawer-header {
padding: 8px 16px;
border-bottom: none;
align-items: stretch;
border-bottom: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
}
.ant-drawer-close {
margin-inline-end: 0px;
}
.ant-drawer-body {
padding: 16px;
}
.title {
color: var(--text-vanilla-400);
font-family: Inter;
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.radio-button {
display: flex;
align-items: center;
justify-content: center;
padding-top: var(--padding-1);
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
.log-detail-drawer__log {
width: 100%;
display: flex;
align-items: center;
gap: 4px;
position: relative;
.log-body {
font-family: 'SF Mono';
font-family: 'Space Mono', monospace;
font-size: var(--font-size-sm);
font-weight: var(--font-weight-normal);
line-height: 18px;
letter-spacing: -0.07px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
color: var(--text-vanilla-400);
opacity: 0.6;
}
.log-type-indicator {
height: 24px;
border: 2px solid var(--bg-slate-400);
border-radius: 5px;
margin-left: 0;
&.INFO {
border-color: #1d212d;
}
&.WARNING {
border-color: #ffcd56;
}
&.ERROR {
border-color: #e5484d;
}
}
.log-overflow-shadow {
background: linear-gradient(270deg, #121317 10.4%, rgba(18, 19, 23, 0) 100%);
width: 196px;
position: absolute;
right: 0;
}
}
.tabs-and-search {
display: flex;
justify-content: space-between;
align-items: center;
margin: 16px 0;
.action-btn {
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: center;
}
.json-action-btn {
display: flex;
gap: 8px;
}
}
.views-tabs {
color: var(--text-vanilla-400);
.view-title {
display: flex;
gap: var(--margin-2);
align-items: center;
justify-content: center;
font-size: var(--font-size-xs);
font-style: normal;
font-weight: var(--font-weight-normal);
}
.tab {
border: 1px solid var(--bg-slate-400);
width: 114px;
}
.tab::before {
background: var(--bg-slate-400);
}
.selected_view {
background: var(--bg-slate-300);
color: var(--text-vanilla-100);
border: 1px solid var(--bg-slate-400);
}
.selected_view::before {
background: var(--bg-slate-400);
}
}
.search-input {
margin-top: var(--margin-2);
border: 1px solid var(--bg-slate-400);
height: 46px;
padding: var(--padding-1) var(--padding-2);
}
}
.lightMode {
.ant-drawer-header {
border-bottom: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-100);
}
.log-detail-drawer {
.title {
color: var(--text-ink-300);
}
.log-detail-drawer__log {
.log-overflow-shadow {
background: linear-gradient(
270deg,
var(--bg-vanilla-100) 10.4%,
rgba(255, 255, 255, 0) 100%
);
}
.log-type-indicator {
border: 2px solid var(--bg-vanilla-400);
}
.ant-typography {
color: var(--text-ink-300);
background: transparent;
}
}
.radio-button {
border: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-100);
color: var(--text-ink-300);
}
.views-tabs {
.tab {
background: var(--bg-vanilla-100);
}
.selected_view {
background: var(--bg-vanilla-300);
border: 1px solid var(--bg-slate-300);
color: var(--text-ink-400);
}
.selected_view::before {
background: var(--bg-vanilla-300);
border-left: 1px solid var(--bg-slate-300);
}
}
.tabs-and-search {
.action-btn {
border: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-100);
color: var(--text-ink-300);
}
}
.search-input {
border: 1px solid var(--bg-vanilla-200);
background: var(--bg-vanilla-100);
color: var(--text-ink-300);
}
}
}

View File

@@ -0,0 +1,10 @@
.query-builder-search-wrapper {
margin-top: 10px;
height: 46px;
border: 1px solid var(--bg-slate-400);
border-bottom: none;
.ant-select-selector {
border: none !important;
}
}

View File

@@ -0,0 +1,77 @@
import './QueryBuilderSearchWrapper.styles.scss';
import useInitialQuery from 'container/LogsExplorerContext/useInitialQuery';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import { Dispatch, SetStateAction, useEffect } from 'react';
import { ILog } from 'types/api/logs/log';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
function QueryBuilderSearchWrapper({
log,
filters,
contextQuery,
isEdit,
suffixIcon,
setFilters,
setContextQuery,
}: QueryBuilderSearchWraperProps): JSX.Element {
const initialContextQuery = useInitialQuery(log);
useEffect(() => {
setContextQuery(initialContextQuery);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleSearch = (tagFilters: TagFilter): void => {
const tagFiltersLength = tagFilters.items.length;
if (
(!tagFiltersLength && (!filters || !filters.items.length)) ||
tagFiltersLength === filters?.items.length ||
!contextQuery
)
return;
const nextQuery: Query = {
...contextQuery,
builder: {
...contextQuery.builder,
queryData: contextQuery.builder.queryData.map((item) => ({
...item,
filters: tagFilters,
})),
},
};
setFilters({ ...tagFilters });
setContextQuery({ ...nextQuery });
};
// eslint-disable-next-line react/jsx-no-useless-fragment
if (!contextQuery || !isEdit) return <></>;
return (
<QueryBuilderSearch
query={contextQuery?.builder.queryData[0]}
onChange={handleSearch}
className="query-builder-search-wrapper"
suffixIcon={suffixIcon}
/>
);
}
interface QueryBuilderSearchWraperProps {
log: ILog;
isEdit: boolean;
contextQuery: Query | undefined;
setContextQuery: Dispatch<SetStateAction<Query | undefined>>;
filters: TagFilter | null;
setFilters: Dispatch<SetStateAction<TagFilter | null>>;
suffixIcon?: React.ReactNode;
}
QueryBuilderSearchWrapper.defaultProps = {
suffixIcon: undefined,
};
export default QueryBuilderSearchWrapper;

View File

@@ -0,0 +1,7 @@
export const VIEW_TYPES = {
OVERVIEW: 'OVERVIEW',
JSON: 'JSON',
CONTEXT: 'CONTEXT',
} as const;
export type VIEWS = typeof VIEW_TYPES[keyof typeof VIEW_TYPES];

View File

@@ -1,50 +1,207 @@
import { Drawer, Tabs } from 'antd';
import JSONView from 'container/LogDetailedView/JsonView';
import TableView from 'container/LogDetailedView/TableView';
import { useMemo } from 'react';
/* eslint-disable sonarjs/cognitive-complexity */
import './LogDetails.styles.scss';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
import { RadioChangeEvent } from 'antd/lib';
import cx from 'classnames';
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
import ContextView from 'container/LogDetailedView/ContextView/ContextView';
import JSONView from 'container/LogDetailedView/JsonView';
import Overview from 'container/LogDetailedView/Overview';
import { aggregateAttributesResourcesToString } from 'container/LogDetailedView/utils';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
import {
Braces,
Copy,
Filter,
HardHat,
Table,
TextSelect,
X,
} from 'lucide-react';
import { useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { VIEW_TYPES, VIEWS } from './constants';
import { LogDetailProps } from './LogDetail.interfaces';
import QueryBuilderSearchWrapper from './QueryBuilderSearchWrapper';
function LogDetail({
log,
onClose,
onAddToQuery,
onClickActionItem,
selectedTab,
}: LogDetailProps): JSX.Element {
const items = useMemo(
() => [
{
label: 'Table',
key: '1',
children: log && (
<TableView
logData={log}
onAddToQuery={onAddToQuery}
onClickActionItem={onClickActionItem}
/>
),
},
{
label: 'JSON',
key: '2',
children: log && <JSONView logData={log} />,
},
],
[log, onAddToQuery, onClickActionItem],
);
const [, copyToClipboard] = useCopyToClipboard();
const [selectedView, setSelectedView] = useState<VIEWS>(selectedTab);
const [isFilterVisibile, setIsFilterVisible] = useState<boolean>(false);
const [contextQuery, setContextQuery] = useState<Query | undefined>();
const [filters, setFilters] = useState<TagFilter | null>(null);
const [isEdit, setIsEdit] = useState<boolean>(false);
const isDarkMode = useIsDarkMode();
const { notifications } = useNotifications();
const LogJsonData = log ? aggregateAttributesResourcesToString(log) : '';
const handleModeChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
setIsEdit(false);
setIsFilterVisible(false);
};
const handleFilterVisible = (): void => {
setIsFilterVisible(!isFilterVisibile);
setIsEdit(!isEdit);
};
const drawerCloseHandler = (
e: React.MouseEvent | React.KeyboardEvent,
): void => {
if (onClose) {
onClose(e);
}
};
const handleJSONCopy = (): void => {
copyToClipboard(LogJsonData);
notifications.success({
message: 'Copied to clipboard',
});
};
if (!log) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <></>;
}
const logType = log?.attributes_string?.log_level || LogType.INFO;
return (
<Drawer
width="60%"
title="Log Details"
title={
<>
<Divider type="vertical" className={cx('log-type-indicator', LogType)} />
<Typography.Text className="title">Log details</Typography.Text>
</>
}
placement="right"
closable
onClose={onClose}
// closable
onClose={drawerCloseHandler}
open={log !== null}
style={{ overscrollBehavior: 'contain' }}
style={{
overscrollBehavior: 'contain',
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
}}
className="log-detail-drawer"
destroyOnClose
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
>
<Tabs defaultActiveKey="1" items={items} />
<div className="log-detail-drawer__log">
<Divider type="vertical" className={cx('log-type-indicator', logType)} />
<Tooltip title={log?.body} placement="left">
<Typography.Text className="log-body">{log?.body}</Typography.Text>
</Tooltip>
<div className="log-overflow-shadow">&nbsp;</div>
</div>
<div className="tabs-and-search">
<Radio.Group
className="views-tabs"
onChange={handleModeChange}
value={selectedView}
>
<Radio.Button
className={
// eslint-disable-next-line sonarjs/no-duplicate-string
selectedView === VIEW_TYPES.OVERVIEW ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.OVERVIEW}
>
<div className="view-title">
<Table size={14} />
Overview
</div>
</Radio.Button>
<Radio.Button
className={selectedView === VIEW_TYPES.JSON ? 'selected_view tab' : 'tab'}
value={VIEW_TYPES.JSON}
>
<div className="view-title">
<Braces size={14} />
JSON
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.CONTEXT ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.CONTEXT}
>
<div className="view-title">
<TextSelect size={14} />
Context
</div>
</Radio.Button>
</Radio.Group>
{selectedView === VIEW_TYPES.JSON && (
<div className="json-action-btn">
<Button
className="action-btn"
icon={<Copy size={16} />}
onClick={handleJSONCopy}
/>
</div>
)}
{selectedView === VIEW_TYPES.CONTEXT && (
<Button
className="action-btn"
icon={<Filter size={16} />}
onClick={handleFilterVisible}
/>
)}
</div>
<QueryBuilderSearchWrapper
isEdit={isEdit}
log={log}
filters={filters}
setContextQuery={setContextQuery}
setFilters={setFilters}
contextQuery={contextQuery}
suffixIcon={
<HardHat size={12} style={{ paddingRight: Spacing.PADDING_2 }} />
}
/>
{selectedView === VIEW_TYPES.OVERVIEW && (
<Overview
logData={log}
onAddToQuery={onAddToQuery}
onClickActionItem={onClickActionItem}
/>
)}
{selectedView === VIEW_TYPES.JSON && <JSONView logData={log} />}
{selectedView === VIEW_TYPES.CONTEXT && (
<ContextView
log={log}
filters={filters}
contextQuery={contextQuery}
isEdit={isEdit}
/>
)}
</Drawer>
);
}

View File

@@ -0,0 +1,103 @@
.log-field-key {
padding-right: 5px;
color: var(--text-vanilla-400, #c0c1c3);
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
}
.log-value {
color: var(--text-vanilla-400, #c0c1c3);
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
}
.log-line {
display: flex;
overflow: hidden;
.log-state-indicator {
padding-left: 0;
}
transition: background-color 0.2s ease-in;
&:hover {
background-color: rgba(171, 189, 255, 0.04) !important;
}
}
.log-selected-fields {
display: flex;
width: 100%;
overflow: hidden;
align-items: center;
.selected-log-field-key {
color: var(--bg-robin-400) !important;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
}
.selected-log-value {
color: var(--bg-sienna-500);
border-radius: 2px;
background: rgba(173, 127, 88, 0.08);
padding: 0px 2px;
margin-left: 7px;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.07px;
font-size: 14px;
}
.selected-log-kv {
min-height: 24px;
display: flex;
align-items: center;
}
}
.log-action-buttons {
position: absolute;
transform: translate(-50%, -50%);
top: 50%;
right: 0;
cursor: pointer;
height: 32px;
width: 68px;
border-radius: 2px;
border: 1px solid var(--bg-slate-400, #1d212d);
background: var(--bg-ink-400, #121317);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
.context-btn {
width: 50% !important;
}
.copy-link-btn {
width: 50% !important;
border-left: 1px solid var(--bg-slate-400, #1d212d) !important;
}
.ant-btn-default {
border: none;
box-shadow: none;
}
}
.lightMode {
.log-field-key {
color: var(--text-slate-400);
}
.log-value {
color: var(--text-slate-400);
}
.log-line {
&:hover {
background-color: var(--text-vanilla-200) !important;
}
}
}

View File

@@ -1,35 +1,32 @@
import { blue, grey, orange } from '@ant-design/colors';
import {
CopyFilled,
ExpandAltOutlined,
LinkOutlined,
MonitorOutlined,
} from '@ant-design/icons';
import './ListLogView.styles.scss';
import { blue } from '@ant-design/colors';
import Convert from 'ansi-to-html';
import { Button, Divider, Row, Typography } from 'antd';
import LogsExplorerContext from 'container/LogsExplorerContext';
import { Typography } from 'antd';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import dayjs from 'dayjs';
import dompurify from 'dompurify';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useNotifications } from 'hooks/useNotifications';
// utils
import { FlatLogData } from 'lib/logs/flatLogData';
import { useCallback, useMemo } from 'react';
import { useCopyToClipboard } from 'react-use';
import { useCallback, useMemo, useState } from 'react';
// interfaces
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
// components
import AddToQueryHOC, { AddToQueryHOCProps } from '../AddToQueryHOC';
import CopyClipboardHOC from '../CopyClipboardHOC';
import LogLinesActionButtons from '../LogLinesActionButtons/LogLinesActionButtons';
import LogStateIndicator, {
LogType,
} from '../LogStateIndicator/LogStateIndicator';
// styles
import {
Container,
LogContainer,
LogText,
SelectedLog,
Text,
TextContainer,
} from './styles';
@@ -55,12 +52,10 @@ function LogGeneralField({ fieldKey, fieldValue }: LogFieldProps): JSX.Element {
return (
<TextContainer>
<Text ellipsis type="secondary">
{`${fieldKey}: `}
<Text ellipsis type="secondary" className="log-field-key">
{`${fieldKey} : `}
</Text>
<CopyClipboardHOC textToCopy={fieldValue}>
<LogText dangerouslySetInnerHTML={html} />
</CopyClipboardHOC>
<LogText dangerouslySetInnerHTML={html} className="log-value" />
</TextContainer>
);
}
@@ -71,23 +66,23 @@ function LogSelectedField({
onAddToQuery,
}: LogSelectedFieldProps): JSX.Element {
return (
<SelectedLog>
<div className="log-selected-fields">
<AddToQueryHOC
fieldKey={fieldKey}
fieldValue={fieldValue}
onAddToQuery={onAddToQuery}
>
<Typography.Text>
<span style={{ color: blue[4] }}>{fieldKey}</span>
<span style={{ color: blue[4] }} className="selected-log-field-key">
{fieldKey}
</span>
</Typography.Text>
</AddToQueryHOC>
<CopyClipboardHOC textToCopy={fieldValue}>
<Typography.Text ellipsis>
<span>{': '}</span>
<span style={{ color: orange[6] }}>{fieldValue || "''"}</span>
</Typography.Text>
</CopyClipboardHOC>
</SelectedLog>
<Typography.Text ellipsis className="selected-log-kv">
<span className="selected-log-field-key">{': '}</span>
<span className="selected-log-value">{fieldValue || "''"}</span>
</Typography.Text>
</div>
);
}
@@ -106,31 +101,38 @@ function ListLogView({
}: ListLogViewProps): JSX.Element {
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
const [, setCopy] = useCopyToClipboard();
const { notifications } = useNotifications();
const [hasActionButtons, setHasActionButtons] = useState<boolean>(false);
const { isHighlighted, isLogsExplorerPage, onLogCopy } = useCopyLogLink(
logData.id,
);
const {
activeLog: activeContextLog,
onAddToQuery: handleAddToQuery,
onSetActiveLog: handleSetActiveContextLog,
onClearActiveLog: handleClearActiveContextLog,
} = useActiveLog();
const handlerClearActiveContextLog = useCallback(
(event: React.MouseEvent | React.KeyboardEvent) => {
event.preventDefault();
event.stopPropagation();
handleClearActiveContextLog();
},
[handleClearActiveContextLog],
);
const handleDetailedView = useCallback(() => {
onSetActiveLog(logData);
}, [logData, onSetActiveLog]);
const handleShowContext = useCallback(() => {
handleSetActiveContextLog(logData);
}, [logData, handleSetActiveContextLog]);
const handleCopyJSON = (): void => {
setCopy(JSON.stringify(logData, null, 2));
notifications.success({
message: 'Copied to clipboard',
});
};
const handleShowContext = useCallback(
(event: React.MouseEvent) => {
event.preventDefault();
event.stopPropagation();
handleSetActiveContextLog(logData);
},
[logData, handleSetActiveContextLog],
);
const updatedSelecedFields = useMemo(
() => selectedFields.filter((e) => e.name !== 'id'),
@@ -145,83 +147,64 @@ function ListLogView({
[flattenLogData.timestamp],
);
const logType = logData?.attributes_string?.log_level || LogType.INFO;
const handleMouseEnter = (): void => {
setHasActionButtons(true);
};
const handleMouseLeave = (): void => {
setHasActionButtons(false);
};
return (
<Container $isActiveLog={isHighlighted}>
<div>
<LogContainer>
<>
<LogGeneralField fieldKey="log" fieldValue={flattenLogData.body} />
{flattenLogData.stream && (
<LogGeneralField fieldKey="stream" fieldValue={flattenLogData.stream} />
)}
<LogGeneralField fieldKey="timestamp" fieldValue={timestampValue} />
</>
</LogContainer>
<div>
{updatedSelecedFields.map((field) =>
isValidLogField(flattenLogData[field.name] as never) ? (
<LogSelectedField
key={field.name}
fieldKey={field.name}
fieldValue={flattenLogData[field.name] as never}
onAddToQuery={onAddToQuery}
/>
) : null,
)}
<>
<Container
$isActiveLog={isHighlighted}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={handleDetailedView}
>
<div className="log-line">
<LogStateIndicator type={logType} />
<div>
<LogContainer>
<LogGeneralField fieldKey="Log" fieldValue={flattenLogData.body} />
{flattenLogData.stream && (
<LogGeneralField fieldKey="Stream" fieldValue={flattenLogData.stream} />
)}
<LogGeneralField fieldKey="Timestamp" fieldValue={timestampValue} />
{updatedSelecedFields.map((field) =>
isValidLogField(flattenLogData[field.name] as never) ? (
<LogSelectedField
key={field.name}
fieldKey={field.name}
fieldValue={flattenLogData[field.name] as never}
onAddToQuery={onAddToQuery}
/>
) : null,
)}
</LogContainer>
</div>
</div>
</div>
<Divider style={{ padding: 0, margin: '0.4rem 0', opacity: 0.5 }} />
<Row>
<Button
size="small"
type="text"
onClick={handleDetailedView}
style={{ color: blue[5] }}
icon={<ExpandAltOutlined />}
>
View Details
</Button>
<Button
size="small"
type="text"
onClick={handleCopyJSON}
style={{ color: grey[1] }}
icon={<CopyFilled />}
>
Copy JSON
</Button>
{isLogsExplorerPage && (
<>
<Button
size="small"
type="text"
onClick={handleShowContext}
style={{ color: grey[1] }}
icon={<MonitorOutlined />}
>
Show in Context
</Button>
<Button
size="small"
type="text"
onClick={onLogCopy}
style={{ color: grey[1] }}
icon={<LinkOutlined />}
>
Copy Link
</Button>
</>
)}
{activeContextLog && (
<LogsExplorerContext
log={activeContextLog}
onClose={handleClearActiveContextLog}
{hasActionButtons && isLogsExplorerPage && (
<LogLinesActionButtons
handleShowContext={handleShowContext}
onLogCopy={onLogCopy}
/>
)}
</Row>
</Container>
</Container>
{activeContextLog && (
<LogDetail
log={activeContextLog}
onAddToQuery={handleAddToQuery}
selectedTab={VIEW_TYPES.CONTEXT}
onClose={handlerClearActiveContextLog}
/>
)}
</>
);
}

View File

@@ -7,6 +7,7 @@ export const Container = styled(Card)<{
}>`
width: 100% !important;
margin-bottom: 0.3rem;
cursor: pointer;
.ant-card-body {
padding: 0.3rem 0.6rem;
}
@@ -29,11 +30,13 @@ export const TextContainer = styled.div`
export const LogContainer = styled.div`
margin-left: 0.5rem;
display: flex;
flex-direction: column;
gap: 6px;
`;
export const LogText = styled.div`
display: inline-block;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;

View File

@@ -0,0 +1,44 @@
.log-line-action-buttons {
display: flex;
position: absolute;
transform: translate(-50%, -50%);
top: 50%;
right: 0;
cursor: pointer;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-400);
.ant-btn-default {
border: none;
box-shadow: none;
padding: 9px;
justify-content: center;
align-items: center;
display: flex;
&.active-tab {
background-color: var(--bg-slate-400);
}
}
.copy-log-btn {
border-left: 1px solid var(--bg-slate-400);
border-color: var(--bg-slate-400) !important;
}
}
.lightMode {
.log-line-action-buttons {
border: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-400);
.ant-btn-default {
}
.copy-log-btn {
border-left: 1px solid var(--bg-vanilla-400);
border-color: var(--bg-vanilla-400) !important;
}
}
}

View File

@@ -0,0 +1,42 @@
import './LogLinesActionButtons.styles.scss';
import { LinkOutlined } from '@ant-design/icons';
import { Button, Tooltip } from 'antd';
import { TextSelect } from 'lucide-react';
import { MouseEventHandler } from 'react';
export interface LogLinesActionButtonsProps {
handleShowContext: MouseEventHandler<HTMLElement>;
onLogCopy: MouseEventHandler<HTMLElement>;
customClassName?: string;
}
export default function LogLinesActionButtons({
handleShowContext,
onLogCopy,
customClassName = '',
}: LogLinesActionButtonsProps): JSX.Element {
return (
<div className={`log-line-action-buttons ${customClassName}`}>
<Tooltip title="Show Context">
<Button
size="small"
icon={<TextSelect size={14} />}
className="show-context-btn"
onClick={handleShowContext}
/>
</Tooltip>
<Tooltip title="Copy Link">
<Button
size="small"
icon={<LinkOutlined size={14} />}
onClick={onLogCopy}
className="copy-log-btn"
/>
</Tooltip>
</div>
);
}
LogLinesActionButtons.defaultProps = {
customClassName: '',
};

View File

@@ -0,0 +1,34 @@
.log-state-indicator {
padding-left: 8px;
.line {
margin: 0 8px;
min-height: 24px;
height: 100%;
width: 3px;
border-radius: 50px;
background-color: transparent;
&.INFO {
background-color: #1d212d;
}
&.WARNING {
background-color: #ffcd56;
}
&.ERROR {
background-color: #e5484d;
}
}
&.isActive {
.dot {
color: var(--bg-robin-400, #7190f9);
}
.line {
background-color: var(--bg-robin-400, #7190f9);
}
}
}

View File

@@ -0,0 +1,28 @@
import './LogStateIndicator.styles.scss';
import cx from 'classnames';
export const LogType = {
INFO: 'INFO',
WARNING: 'WARNING',
ERROR: 'ERROR',
};
function LogStateIndicator({
type,
isActive,
}: {
type: string;
isActive?: boolean;
}): JSX.Element {
return (
<div className={cx('log-state-indicator', isActive ? 'isActive' : '')}>
<div className={cx('line', type)}> </div>
</div>
);
}
LogStateIndicator.defaultProps = {
isActive: false,
};
export default LogStateIndicator;

View File

@@ -1,11 +1,9 @@
import {
ExpandAltOutlined,
LinkOutlined,
MonitorOutlined,
} from '@ant-design/icons';
import './RawLogView.styles.scss';
import Convert from 'ansi-to-html';
import { Button, DrawerProps, Tooltip } from 'antd';
import { DrawerProps } from 'antd';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES, VIEWS } from 'components/LogDetail/constants';
import LogsExplorerContext from 'container/LogsExplorerContext';
import dayjs from 'dayjs';
import dompurify from 'dompurify';
@@ -22,13 +20,12 @@ import {
useState,
} from 'react';
import LogLinesActionButtons from '../LogLinesActionButtons/LogLinesActionButtons';
import LogStateIndicator, {
LogType,
} from '../LogStateIndicator/LogStateIndicator';
// styles
import {
ActionButtonsWrapper,
ExpandIconWrapper,
RawLogContent,
RawLogViewContainer,
} from './styles';
import { RawLogContent, RawLogViewContainer } from './styles';
import { RawLogViewProps } from './types';
const convert = new Convert();
@@ -45,7 +42,6 @@ function RawLogView({
);
const {
activeLog: activeContextLog,
onSetActiveLog: handleSetActiveContextLog,
onClearActiveLog: handleClearActiveContextLog,
} = useActiveLog();
const {
@@ -56,12 +52,15 @@ function RawLogView({
} = useActiveLog();
const [hasActionButtons, setHasActionButtons] = useState<boolean>(false);
const [selectedTab, setSelectedTab] = useState<VIEWS | undefined>();
const isDarkMode = useIsDarkMode();
const isReadOnlyLog = !isLogsExplorerPage || isReadOnly;
const severityText = data.severity_text ? `${data.severity_text} |` : '';
const logType = data?.attributes_string?.log_level || LogType.INFO;
const text = useMemo(
() =>
typeof data.timestamp === 'string'
@@ -74,6 +73,7 @@ function RawLogView({
if (activeContextLog || isReadOnly) return;
onSetActiveLog(data);
setSelectedTab(VIEW_TYPES.OVERVIEW);
}, [activeContextLog, isReadOnly, data, onSetActiveLog]);
const handleCloseLogDetail: DrawerProps['onClose'] = useCallback(
@@ -84,6 +84,7 @@ function RawLogView({
event.stopPropagation();
onClearActiveLog();
setSelectedTab(undefined);
},
[onClearActiveLog],
);
@@ -104,9 +105,11 @@ function RawLogView({
(event) => {
event.preventDefault();
event.stopPropagation();
handleSetActiveContextLog(data);
// handleSetActiveContextLog(data);
setSelectedTab(VIEW_TYPES.CONTEXT);
onSetActiveLog(data);
},
[data, handleSetActiveContextLog],
[data, onSetActiveLog],
);
const html = useMemo(
@@ -123,37 +126,27 @@ function RawLogView({
align="middle"
$isDarkMode={isDarkMode}
$isReadOnly={isReadOnly}
$isActiveLog={isHighlighted}
$isHightlightedLog={isHighlighted}
$isActiveLog={isActiveLog}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{!isReadOnly && (
<ExpandIconWrapper flex="30px">
<ExpandAltOutlined />
</ExpandIconWrapper>
)}
<LogStateIndicator type={logType} />
<RawLogContent
$isReadOnly={isReadOnly}
$isActiveLog={isActiveLog}
$isDarkMode={isDarkMode}
$isTextOverflowEllipsisDisabled={isTextOverflowEllipsisDisabled}
linesPerRow={linesPerRow}
dangerouslySetInnerHTML={html}
/>
{hasActionButtons && (
<ActionButtonsWrapper>
<Tooltip title="Show Context">
<Button
size="small"
icon={<MonitorOutlined />}
onClick={handleShowContext}
/>
</Tooltip>
<Tooltip title="Copy Link">
<Button size="small" icon={<LinkOutlined />} onClick={onLogCopy} />
</Tooltip>
</ActionButtonsWrapper>
<LogLinesActionButtons
handleShowContext={handleShowContext}
onLogCopy={onLogCopy}
/>
)}
{activeContextLog && (
@@ -162,12 +155,15 @@ function RawLogView({
onClose={handleClearActiveContextLog}
/>
)}
<LogDetail
log={activeLog}
onClose={handleCloseLogDetail}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
/>
{selectedTab && (
<LogDetail
selectedTab={selectedTab}
log={activeLog}
onClose={handleCloseLogDetail}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
/>
)}
</RawLogViewContainer>
);
}

View File

@@ -1,4 +1,5 @@
import { blue } from '@ant-design/colors';
import { Color } from '@signozhq/design-tokens';
import { Col, Row, Space } from 'antd';
import styled from 'styled-components';
import { getActiveLogBackground, getDefaultLogBackground } from 'utils/logs';
@@ -9,20 +10,21 @@ export const RawLogViewContainer = styled(Row)<{
$isDarkMode: boolean;
$isReadOnly?: boolean;
$isActiveLog?: boolean;
$isHightlightedLog: boolean;
}>`
position: relative;
width: 100%;
font-weight: 700;
font-size: 0.625rem;
line-height: 1.25rem;
display: flex;
alignitems: center;
transition: background-color 0.2s ease-in;
${({ $isActiveLog }): string => getActiveLogBackground($isActiveLog)}
${({ $isReadOnly, $isDarkMode, $isActiveLog }): string =>
${({ $isReadOnly, $isActiveLog, $isDarkMode }): string =>
$isActiveLog
? getActiveLogBackground()
? getActiveLogBackground($isActiveLog, $isDarkMode)
: getDefaultLogBackground($isReadOnly, $isDarkMode)}
`;
@@ -30,13 +32,17 @@ export const ExpandIconWrapper = styled(Col)`
color: ${blue[6]};
padding: 0.25rem 0.375rem;
cursor: pointer;
font-size: 12px;
`;
export const RawLogContent = styled.div<RawLogContentProps>`
margin-bottom: 0;
font-family: Fira Code, monospace;
font-weight: 300;
font-family: 'SF Mono', monospace;
font-family: 'Space Mono', monospace;
font-size: 13px;
font-weight: 400;
text-align: left;
color: ${({ $isDarkMode }): string =>
$isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400};
${({ $isTextOverflowEllipsisDisabled, linesPerRow }): string =>
$isTextOverflowEllipsisDisabled
@@ -48,15 +54,12 @@ export const RawLogContent = styled.div<RawLogContentProps>`
line-clamp: ${linesPerRow};
-webkit-box-orient: vertical;`};
font-size: 12px;
line-height: 24px;
letter-spacing: -0.07px;
padding: 4px;
cursor: ${({ $isActiveLog, $isReadOnly }): string =>
$isActiveLog || $isReadOnly ? 'initial' : 'pointer'};
${({ $isActiveLog, $isReadOnly }): string =>
$isReadOnly && $isActiveLog ? 'padding: 0 1.5rem;' : ''}
`;
export const ActionButtonsWrapper = styled(Space)`

View File

@@ -12,5 +12,6 @@ export interface RawLogContentProps {
linesPerRow: number;
$isReadOnly?: boolean;
$isActiveLog?: boolean;
$isDarkMode?: boolean;
$isTextOverflowEllipsisDisabled?: boolean;
}

View File

@@ -1,12 +1,21 @@
import { TableProps } from 'antd';
import { CSSProperties } from 'react';
export const defaultCellStyle: CSSProperties = {
paddingTop: 4,
paddingBottom: 6,
paddingRight: 8,
paddingLeft: 8,
};
export function getDefaultCellStyle(isDarkMode?: boolean): CSSProperties {
return {
paddingTop: 4,
paddingBottom: 6,
paddingRight: 8,
paddingLeft: 8,
color: isDarkMode ? 'var(--bg-vanilla-400)' : 'var(--bg-slate-400)',
fontSize: '14px',
fontStyle: 'normal',
fontWeight: 400,
lineHeight: '18px',
letterSpacing: '-0.07px',
marginBottom: '0px',
};
}
export const defaultTableStyle: CSSProperties = {
minWidth: '40rem',

View File

@@ -2,18 +2,22 @@ import styled from 'styled-components';
interface TableBodyContentProps {
linesPerRow: number;
isDarkMode?: boolean;
}
export const TableBodyContent = styled.div<TableBodyContentProps>`
margin-bottom: 0;
color: ${(props): string =>
props.isDarkMode ? 'var(--bg-vanilla-400, #c0c1c3)' : 'var(--bg-slate-400)'};
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: ${(props): number => props.linesPerRow};
line-clamp: ${(props): number => props.linesPerRow};
-webkit-box-orient: vertical;
font-size: 0.875rem;
line-height: 2rem;
`;

View File

@@ -0,0 +1,27 @@
.text {
color: var(--bg-vanilla-400);
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
}
.table-timestamp {
display: flex;
align-items: center;
.ant-typography {
margin-bottom: 0;
}
.log-state-indicator {
padding: 0px;
}
}
.lightMode {
.text {
color: var(--bg-slate-400);
}
}

View File

@@ -1,22 +1,18 @@
import {
ExpandAltOutlined,
LinkOutlined,
MonitorOutlined,
} from '@ant-design/icons';
import './useTableView.styles.scss';
import Convert from 'ansi-to-html';
import { Button, Space, Typography } from 'antd';
import { Typography } from 'antd';
import { ColumnsType } from 'antd/es/table';
import dayjs from 'dayjs';
import dompurify from 'dompurify';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { FlatLogData } from 'lib/logs/flatLogData';
import { useCallback, useMemo } from 'react';
import { useMemo } from 'react';
import { ExpandIconWrapper } from '../RawLogView/styles';
import { defaultCellStyle, defaultTableStyle } from './config';
import LogStateIndicator from '../LogStateIndicator/LogStateIndicator';
import { defaultTableStyle, getDefaultCellStyle } from './config';
import { TableBodyContent } from './styles';
import {
ActionsColumnProps,
ColumnTypeRender,
UseTableViewProps,
UseTableViewResult,
@@ -24,60 +20,15 @@ import {
const convert = new Convert();
function ActionsColumn({
logId,
logs,
onOpenLogsContext,
}: ActionsColumnProps): JSX.Element {
const currentLog = useMemo(() => logs.find(({ id }) => id === logId), [
logs,
logId,
]);
const { onLogCopy } = useCopyLogLink(currentLog?.id);
const handleShowContext = useCallback(() => {
if (!onOpenLogsContext || !currentLog) return;
onOpenLogsContext(currentLog);
}, [currentLog, onOpenLogsContext]);
return (
<Space>
<Button
size="small"
onClick={handleShowContext}
icon={<MonitorOutlined />}
/>
<Button size="small" onClick={onLogCopy} icon={<LinkOutlined />} />
</Space>
);
}
export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
const {
logs,
fields,
linesPerRow,
appendTo = 'center',
onOpenLogsContext,
onClickExpand,
} = props;
const { isLogsExplorerPage } = useCopyLogLink();
const { logs, fields, linesPerRow, appendTo = 'center' } = props;
const isDarkMode = useIsDarkMode();
const flattenLogData = useMemo(() => logs.map((log) => FlatLogData(log)), [
logs,
]);
const handleClickExpand = useCallback(
(index: number): void => {
if (!onClickExpand) return;
onClickExpand(logs[index]);
},
[logs, onClickExpand],
);
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
const fieldColumns: ColumnsType<Record<string, unknown>> = fields
.filter((e) => e.name !== 'id')
@@ -87,7 +38,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
key: name,
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
props: {
style: defaultCellStyle,
style: getDefaultCellStyle(isDarkMode),
},
children: (
<Typography.Paragraph ellipsis={{ rows: linesPerRow }}>
@@ -98,38 +49,25 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
}));
return [
{
title: '',
dataIndex: 'id',
key: 'expand',
// https://github.com/ant-design/ant-design/discussions/36886
render: (_, item, index): ColumnTypeRender<Record<string, unknown>> => ({
props: {
style: defaultCellStyle,
},
children: (
<ExpandIconWrapper
onClick={(): void => {
handleClickExpand(index);
}}
>
<ExpandAltOutlined />
</ExpandIconWrapper>
),
}),
},
{
title: 'timestamp',
dataIndex: 'timestamp',
key: 'timestamp',
// https://github.com/ant-design/ant-design/discussions/36886
render: (field): ColumnTypeRender<Record<string, unknown>> => {
render: (field, item): ColumnTypeRender<Record<string, unknown>> => {
const date =
typeof field === 'string'
? dayjs(field).format()
: dayjs(field / 1e6).format();
return {
children: <Typography.Paragraph ellipsis>{date}</Typography.Paragraph>,
children: (
<div className="table-timestamp">
<LogStateIndicator type={item.log_level as string} />
<Typography.Paragraph ellipsis className="text">
{date}
</Typography.Paragraph>
</div>
),
};
},
},
@@ -148,39 +86,14 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
__html: convert.toHtml(dompurify.sanitize(field)),
}}
linesPerRow={linesPerRow}
isDarkMode={isDarkMode}
/>
),
}),
},
...(appendTo === 'end' ? fieldColumns : []),
...(isLogsExplorerPage
? ([
{
title: 'actions',
dataIndex: 'actions',
key: 'actions',
render: (_, log): ColumnTypeRender<Record<string, unknown>> => ({
children: (
<ActionsColumn
logId={(log.id as unknown) as string}
logs={logs}
onOpenLogsContext={onOpenLogsContext}
/>
),
}),
},
] as ColumnsType<Record<string, unknown>>)
: []),
];
}, [
logs,
fields,
appendTo,
linesPerRow,
isLogsExplorerPage,
handleClickExpand,
onOpenLogsContext,
]);
}, [fields, appendTo, isDarkMode, linesPerRow]);
return { columns, dataSource: flattenLogData };
};

View File

@@ -0,0 +1,335 @@
.nested-menu-container {
z-index: 2;
position: absolute;
right: -2px;
margin: 6px 0;
width: 160px;
border-radius: 4px;
border: 1px solid var(--bg-slate-400, #1d212d);
background: linear-gradient(
139deg,
rgba(18, 19, 23, 0.8) 0%,
rgba(18, 19, 23, 0.9) 98.68%
);
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
.menu-container {
padding: 12px;
.title {
font-family: Inter;
font-size: 11px;
font-weight: 600;
line-height: 18px;
letter-spacing: 0.08em;
text-align: left;
color: var(--bg-slate-200, #52575c);
}
.menu-items {
display: flex;
gap: 12px;
flex-direction: column;
margin-top: 12px;
}
.item {
font-family: Inter;
font-size: 13px;
font-weight: 400;
line-height: 17px;
letter-spacing: 0.01em;
text-align: left;
.item-label {
display: flex;
color: var(--bg-vanilla-400, #c0c1c3);
justify-content: space-between;
}
cursor: pointer;
}
}
.selected-item-content-container {
.add-new-column-header {
padding: 8px;
}
.title {
color: var(--bg-slate-200, #52575c);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 600;
line-height: 18px; /* 163.636% */
letter-spacing: 0.88px;
text-transform: uppercase;
margin-bottom: 12px;
display: flex;
justify-content: space-between;
align-items: center;
.lucide {
color: var(--bg-vanilla-400, #c0c1c3);
cursor: pointer;
}
}
.horizontal-line {
height: 1px;
background: #1d212d;
}
.loading-container {
margin: 12px 0;
}
.item-content {
padding: 12px;
.max-lines-per-row-input {
display: flex;
border: 1px solid var(--bg-slate-400, #1d212d);
.ant-input-number-handler-wrap {
display: none;
}
.ant-input-number {
min-width: 36px;
width: auto;
border: 0px;
text-align: center;
height: 26px;
border-radius: 0;
&:active,
&:focus {
border: none;
box-shadow: none;
}
}
.ant-input-number-focused {
box-shadow: none !important;
}
.ant-input-number-input-wrap {
input {
text-align: center;
font-size: 13px;
&:active,
&:focus {
border: none;
}
}
&:active,
&:focus {
border: none;
}
}
.periscope-btn {
box-shadow: none;
padding: 6px 12px;
height: 26px;
border-radius: 0px 1px 1px 0px;
background: var(--bg-ink-300, #16181d);
}
}
.column-format,
.column-format-new-options {
display: flex;
gap: 12px;
flex-direction: column;
margin-top: 12px;
.column-name {
color: var(--bg-vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
cursor: pointer;
.name {
flex: 1;
overflow: hidden;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
}
.delete-btn {
display: none;
flex: 0 0 16px;
cursor: pointer;
}
&:hover {
.delete-btn {
display: block;
}
}
}
overflow-x: hidden;
&::-webkit-scrollbar {
height: 1rem;
width: 0.2rem;
}
}
.column-format {
max-height: 150px;
overflow: auto;
overflow-x: hidden;
}
.column-format-new-options {
max-height: 150px;
overflow-y: auto;
overflow-x: hidden;
}
.column-divider {
margin: 12px 0;
border-top: 2px solid #1d212d;
}
}
}
&.active {
.nested-menu-container {
backdrop-filter: blur(18px);
.item {
.item-label {
color: var(--bg-vanilla-400, #c0c1c3);
}
}
}
.selected-item-content-container {
width: 110%;
margin-left: -5%;
border-radius: 4px;
border: 1px solid var(--bg-slate-400, #1d212d);
background: linear-gradient(
139deg,
rgba(18, 19, 23, 0.8) 0%,
rgba(18, 19, 23, 0.9) 98.68%
);
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
.column-format {
margin-top: 0px;
}
}
}
}
.lightMode {
.nested-menu-container {
border: 1px solid var(--bg-vanilla-300);
background: linear-gradient(
139deg,
rgba(255, 255, 255, 0.8) 0%,
rgba(255, 255, 255, 0.9) 98.68%
);
box-shadow: 4px 10px 16px 2px rgba(255, 255, 255, 0.2);
.menu-container {
.title {
color: var(--bg-ink-200);
}
.item {
.item-label {
color: var(--bg-ink-400);
}
}
}
.selected-item-content-container {
.title {
color: var(--bg-ink-200);
.lucide {
color: var(--bg-ink-300);
}
}
.horizontal-line {
background: var(--bg-vanilla-300);
}
.item-content {
.max-lines-per-row-input {
border: 1px solid var(--bg-vanilla-300);
.periscope-btn {
background: var(--bg-vanilla-300);
}
}
.column-format,
.column-format-new-options {
.column-name {
color: var(--bg-ink-300);
}
}
}
}
&.active {
.nested-menu-container {
backdrop-filter: blur(18px);
.item {
.item-label {
color: var(--bg-ink-300);
}
}
}
.selected-item-content-container {
border: 1px solid var(--bg-vanilla-300);
background: linear-gradient(
139deg,
rgba(255, 255, 255, 0.8) 0%,
rgba(255, 255, 255, 0.9) 98.68%
);
box-shadow: 4px 10px 16px 2px rgba(255, 255, 255, 0.2);
}
}
}
}

View File

@@ -0,0 +1,252 @@
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import './LogsFormatOptionsMenu.styles.scss';
import { Divider, Input, InputNumber, Tooltip } from 'antd';
import cx from 'classnames';
import { LogViewMode } from 'container/LogsTable';
import { OptionsMenuConfig } from 'container/OptionsMenu/types';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { Check, Minus, Plus, X } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
interface LogsFormatOptionsMenuProps {
title: string;
items: any;
selectedOptionFormat: any;
config: OptionsMenuConfig;
}
export default function LogsFormatOptionsMenu({
title,
items,
selectedOptionFormat,
config,
}: LogsFormatOptionsMenuProps): JSX.Element {
const { maxLines, format, addColumn } = config;
const [selectedItem, setSelectedItem] = useState(selectedOptionFormat);
const maxLinesNumber = (maxLines?.value as number) || 1;
const [maxLinesPerRow, setMaxLinesPerRow] = useState<number>(maxLinesNumber);
const [addNewColumn, setAddNewColumn] = useState(false);
const onChange = useCallback(
(key: LogViewMode) => {
if (!format) return;
format.onChange(key);
},
[format],
);
const handleMenuItemClick = (key: LogViewMode): void => {
setSelectedItem(key);
onChange(key);
setAddNewColumn(false);
};
const incrementMaxLinesPerRow = (): void => {
if (maxLinesPerRow < 10) {
setMaxLinesPerRow(maxLinesPerRow + 1);
}
};
const decrementMaxLinesPerRow = (): void => {
if (maxLinesPerRow > 1) {
setMaxLinesPerRow(maxLinesPerRow - 1);
}
};
const handleSearchValueChange = useDebouncedFn((event): void => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const value = event?.target?.value || '';
if (addColumn && addColumn?.onSearch) {
addColumn?.onSearch(value);
}
}, 300);
const handleToggleAddNewColumn = (): void => {
setAddNewColumn(!addNewColumn);
};
// console.log('optionsMenuConfig', config);
const handleLinesPerRowChange = (maxLinesPerRow: number | null): void => {
if (
maxLinesPerRow &&
Number.isInteger(maxLinesNumber) &&
maxLinesPerRow > 1
) {
setMaxLinesPerRow(maxLinesPerRow);
}
};
useEffect(() => {
if (maxLinesPerRow && config && config.maxLines?.onChange) {
config.maxLines.onChange(maxLinesPerRow);
}
}, [maxLinesPerRow]);
return (
<div
className={cx(
'nested-menu-container',
addNewColumn && selectedItem !== 'raw' ? 'active' : '',
)}
onClick={(event): void => {
// this is to restrict click events to propogate to parent
event.stopPropagation();
}}
>
<div className="menu-container">
<div className="title"> {title} </div>
<div className="menu-items">
{items.map(
(item: any): JSX.Element => (
<div
className="item"
key={item.label}
onClick={(): void => handleMenuItemClick(item.key)}
>
<div className={cx('item-label')}>
{item.label}
{selectedItem === item.key && <Check size={12} />}
</div>
</div>
),
)}
</div>
</div>
{selectedItem && (
<div className="selected-item-content-container active">
{!addNewColumn && <div className="horizontal-line" />}
{addNewColumn && selectedItem !== 'raw' && (
<div className="add-new-column-header">
<div className="title">
{' '}
columns
<X size={14} onClick={handleToggleAddNewColumn} />{' '}
</div>
<Input
tabIndex={0}
type="text"
autoFocus
onFocus={addColumn?.onFocus}
// onBlur={addColumn?.onBlur}
onChange={handleSearchValueChange}
placeholder="Search..."
/>
</div>
)}
<div className="item-content">
{selectedItem === 'raw' && (
<>
<div className="title"> max lines per row </div>
<div className="raw-format max-lines-per-row-input">
<button
type="button"
className="periscope-btn"
onClick={decrementMaxLinesPerRow}
>
{' '}
<Minus size={12} />{' '}
</button>
<InputNumber
min={1}
max={10}
value={maxLinesPerRow}
onChange={handleLinesPerRowChange}
/>
<button
type="button"
className="periscope-btn"
onClick={incrementMaxLinesPerRow}
>
{' '}
<Plus size={12} />{' '}
</button>
</div>
</>
)}
{(selectedItem === 'table' || selectedItem === 'list') && (
<>
{!addNewColumn && (
<div className="title">
columns
<Plus size={14} onClick={handleToggleAddNewColumn} />{' '}
</div>
)}
<div className="column-format">
{addColumn?.value?.map(({ key, id }) => (
<div className="column-name" key={id}>
<div className="name">
<Tooltip placement="left" title={key}>
{key}
</Tooltip>
</div>
<X
className="delete-btn"
size={14}
onClick={(): void => addColumn.onRemove(id as string)}
/>
</div>
))}
</div>
{addColumn?.isFetching && (
<div className="loading-container"> Loading ... </div>
)}
{addNewColumn &&
addColumn &&
addColumn.value.length > 0 &&
addColumn.options &&
addColumn?.options?.length > 0 && (
<Divider className="column-divider" />
)}
{addNewColumn && (
<div className="column-format-new-options">
{addColumn?.options?.map(({ label, value }) => (
<div
className="column-name"
key={value}
onClick={(eve): void => {
console.log('coluimn name', label, value);
eve.stopPropagation();
if (addColumn && addColumn?.onSelect) {
addColumn?.onSelect(value, { label, disabled: false });
}
}}
>
<div className="name">
<Tooltip placement="left" title={label}>
{label}
</Tooltip>
</div>
</div>
))}
</div>
)}
</>
)}
</div>
</div>
)}
</div>
);
}

View File

@@ -1,12 +1,20 @@
@import '@signozhq/design-tokens';
.app-layout {
position: relative;
height: 100%;
width: 100%;
.app-content {
width: 100%;
overflow: auto;
.content-container {
position: relative;
margin: 0 1rem;
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
}
}
}

View File

@@ -230,6 +230,21 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
}
};
const isLogsView = (): boolean =>
routeKey === 'LOGS' ||
routeKey === 'LOGS_EXPLORER' ||
routeKey === 'LOGS_PIPELINES';
useEffect(() => {
if (isDarkMode) {
document.body.classList.remove('lightMode');
document.body.classList.add('darkMode');
} else {
document.body.classList.add('lightMode');
document.body.classList.remove('darkMode');
}
}, [isDarkMode]);
return (
<Layout className={isDarkMode ? 'darkMode' : 'lightMode'}>
<Helmet>
@@ -264,7 +279,11 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
<div className="app-content">
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
<LayoutContent>
<ChildrenContainer>
<ChildrenContainer
style={{
margin: isLogsView() ? 0 : ' 0 1rem',
}}
>
{isToDisplayLayout && !renderFullScreen && <TopNav />}
{children}
</ChildrenContainer>

View File

@@ -17,7 +17,6 @@ export const LayoutContent = styled(LayoutComponent.Content)`
`;
export const ChildrenContainer = styled.div`
margin: 0 1rem;
display: flex;
flex-direction: column;
height: 100%;

View File

@@ -7,16 +7,18 @@ function EditRules({ initialValue, ruleId }: EditRulesProps): JSX.Element {
const [formInstance] = Form.useForm();
return (
<FormAlertRules
alertType={
initialValue.alertType
? (initialValue.alertType as AlertTypes)
: AlertTypes.METRICS_BASED_ALERT
}
formInstance={formInstance}
initialValue={initialValue}
ruleId={ruleId}
/>
<div style={{ marginTop: '1rem' }}>
<FormAlertRules
alertType={
initialValue.alertType
? (initialValue.alertType as AlertTypes)
: AlertTypes.METRICS_BASED_ALERT
}
formInstance={formInstance}
initialValue={initialValue}
ruleId={ruleId}
/>
</div>
);
}

View File

@@ -0,0 +1,87 @@
.explorer-options {
display: flex;
gap: 16px;
padding: 8px 16px;
border-radius: 50px;
border: 1px solid var(--bg-slate-400, #1d212d);
background: rgba(22, 24, 29, 0.6);
box-shadow: 4px 4px 16px 4px rgba(0, 0, 0, 0.25);
backdrop-filter: blur(20px);
position: fixed;
bottom: 16px;
left: calc(50% + 240px);
transform: translate(calc(-50% - 120px), 0);
hr {
border-color: #1d212d;
}
.view-options,
.actions {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
button {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
border: 1px solid #1d2023;
color: #c0c1c3;
background-color: #161922;
box-shadow: none !important;
&.ant-btn-round {
padding-inline-start: 10px;
padding-inline-end: 8px;
font-weight: 500;
}
&.ant-btn-round:disabled {
background-color: rgba(209, 209, 209, 0.074);
color: #5f5f5f;
}
}
.ant-select-focused {
border-color: transparent !important;
.ant-select-selector {
border-color: transparent !important;
box-shadow: none !important;
}
}
.ant-select-selector {
border: transparent !important;
background-color: transparent !important;
}
}
}
.lightMode {
.explorer-options {
border: 1px solid var(--bg-vanilla-300);
background: rgba(255, 255, 255, 0.8);
box-shadow: 4px 4px 16px 4px rgba(255, 255, 255, 0.55);
backdrop-filter: blur(20px);
hr {
border-color: var(--bg-vanilla-300);
}
.view-options,
.actions {
button {
border: 1px solid var(--bg-vanilla-300);
color: var(--bg-ink-200);
background-color: var(--bg-vanilla-300);
}
}
}
}

View File

@@ -0,0 +1,233 @@
import './ExplorerOptions.styles.scss';
import { Button, Modal, Select, Tooltip } from 'antd';
import axios from 'axios';
import { getViewDetailsUsingViewKey } from 'components/ExplorerCard/utils';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import ExportPanelContainer from 'container/ExportPanel/ExportPanelContainer';
import { useGetSearchQueryParam } from 'hooks/queryBuilder/useGetSearchQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useGetAllViews } from 'hooks/saveViews/useGetAllViews';
import { useUpdateView } from 'hooks/saveViews/useUpdateView';
import useErrorNotification from 'hooks/useErrorNotification';
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
import { useNotifications } from 'hooks/useNotifications';
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
import { ConciergeBell, Disc3, Plus } from 'lucide-react';
import { useCallback, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { Dashboard } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { popupContainer } from 'utils/selectPopupContainer';
import { DATASOURCE_VS_ROUTES } from './constants';
function ExplorerOptions({
disabled,
isLoading,
onExport,
query,
sourcepage,
}: ExplorerOptionsProps): JSX.Element {
const [isExport, setIsExport] = useState<boolean>(false);
const { notifications } = useNotifications();
const history = useHistory();
const onModalToggle = useCallback((value: boolean) => {
setIsExport(value);
}, []);
const onCreateAlertsHandler = useCallback(() => {
history.push(
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
JSON.stringify(query),
)}`,
);
}, [history, query]);
const onCancel = (value: boolean) => (): void => {
onModalToggle(value);
};
const onAddToDashboard = (): void => {
setIsExport(true);
};
const {
data: viewsData,
isLoading: viewsIsLoading,
error,
isRefetching,
refetch: refetchAllView,
} = useGetAllViews(sourcepage);
const { currentQuery, panelType, isStagedQueryUpdated } = useQueryBuilder();
const viewName = useGetSearchQueryParam(QueryParams.viewName) || '';
const viewKey = useGetSearchQueryParam(QueryParams.viewKey) || '';
const { mutateAsync: updateViewAsync } = useUpdateView({
compositeQuery: mapCompositeQueryFromQuery(currentQuery, panelType),
viewKey,
extraData: '',
sourcePage: sourcepage,
viewName,
});
const showErrorNotification = (err: Error): void => {
notifications.error({
message: axios.isAxiosError(err) ? err.message : SOMETHING_WENT_WRONG,
});
};
const onUpdateQueryHandler = (): void => {
updateViewAsync(
{
compositeQuery: mapCompositeQueryFromQuery(currentQuery, panelType),
viewKey,
extraData: '',
sourcePage: sourcepage,
viewName,
},
{
onSuccess: () => {
notifications.success({
message: 'View Updated Successfully',
});
refetchAllView();
},
onError: (err) => {
showErrorNotification(err);
},
},
);
};
useErrorNotification(error);
const { handleExplorerTabChange } = useHandleExplorerTabChange();
const onMenuItemSelectHandler = useCallback(
({ key }: { key: string }): void => {
const currentViewDetails = getViewDetailsUsingViewKey(
key,
viewsData?.data.data,
);
if (!currentViewDetails) return;
const {
query,
name,
uuid,
panelType: currentPanelType,
} = currentViewDetails;
handleExplorerTabChange(currentPanelType, {
query,
name,
uuid,
});
},
[viewsData, handleExplorerTabChange],
);
const onClearHandler = (): void => {
history.replace(DATASOURCE_VS_ROUTES[sourcepage]);
};
const handleSelect = (
value: string,
option: { key: string; value: string },
): void => {
onMenuItemSelectHandler({
key: option.key,
});
};
const isQueryUpdated = isStagedQueryUpdated(viewsData?.data?.data, viewKey);
return (
<>
<div className="explorer-options">
{viewsData?.data.data && viewsData?.data.data.length && (
<>
<div className="view-options">
<Select<string, { key: string; value: string }>
getPopupContainer={popupContainer}
showSearch
placeholder="Select a view"
optionFilterProp="children"
loading={viewsIsLoading || isRefetching}
optionLabelProp="value"
value={viewName || undefined}
onSelect={handleSelect}
style={{
width: 150,
}}
allowClear
onClear={onClearHandler}
>
{viewsData?.data.data.map((view) => (
<Select.Option key={view.uuid} value={view.name}>
<Tooltip title={view.name} placement="right">
{view.name}
</Tooltip>
</Select.Option>
))}
</Select>
<Button
shape="round"
onClick={onUpdateQueryHandler}
disabled={!isQueryUpdated}
>
<Disc3 size={16} /> Save this view
</Button>
</div>
<hr />
</>
)}
<div className="actions">
<Button disabled={disabled} shape="circle" onClick={onCreateAlertsHandler}>
<ConciergeBell size={16} />
</Button>
<Button disabled={disabled} shape="circle" onClick={onAddToDashboard}>
<Plus size={16} />
</Button>
</div>
</div>
<Modal
footer={null}
onOk={onCancel(false)}
onCancel={onCancel(false)}
open={isExport}
centered
destroyOnClose
>
<ExportPanelContainer
query={query}
isLoading={isLoading}
onExport={onExport}
/>
</Modal>
</>
);
}
export interface ExplorerOptionsProps {
isLoading?: boolean;
onExport: (dashboard: Dashboard | null) => void;
query: Query | null;
disabled: boolean;
sourcepage: DataSource;
}
ExplorerOptions.defaultProps = { isLoading: false };
export default ExplorerOptions;

View File

@@ -0,0 +1,8 @@
import ROUTES from 'constants/routes';
import { DataSource } from 'types/common/queryBuilder';
export const DATASOURCE_VS_ROUTES: Record<DataSource, string> = {
[DataSource.METRICS]: '',
[DataSource.TRACES]: ROUTES.TRACES_EXPLORER,
[DataSource.LOGS]: ROUTES.LOGS_EXPLORER,
};

View File

@@ -16,7 +16,10 @@ import {
} from './styles';
import { filterOptions, getSelectOptions } from './utils';
function ExportPanel({ isLoading, onExport }: ExportPanelProps): JSX.Element {
function ExportPanelContainer({
isLoading,
onExport,
}: ExportPanelProps): JSX.Element {
const { t } = useTranslation(['dashboard']);
const [selectedDashboardId, setSelectedDashboardId] = useState<string | null>(
@@ -118,4 +121,4 @@ function ExportPanel({ isLoading, onExport }: ExportPanelProps): JSX.Element {
);
}
export default ExportPanel;
export default ExportPanelContainer;

View File

@@ -7,7 +7,7 @@ import { useCallback, useState } from 'react';
import { Dashboard } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import ExportPanelContainer from './ExportPanel';
import ExportPanelContainer from './ExportPanelContainer';
function ExportPanel({
isLoading,

View File

@@ -5,6 +5,7 @@ import GridPanelSwitch from 'container/GridPanelSwitch';
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
import { Time } from 'container/TopNav/DateTimeSelection/config';
import { Time as TimeV2 } from 'container/TopNav/DateTimeSelectionV2/config';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
@@ -28,7 +29,7 @@ export interface ChartPreviewProps {
query: Query | null;
graphType?: PANEL_TYPES;
selectedTime?: timePreferenceType;
selectedInterval?: Time;
selectedInterval?: Time | TimeV2;
headline?: JSX.Element;
alertDef?: AlertDef;
userQueryKey?: string;

View File

@@ -0,0 +1,56 @@
.alert-tabs {
.ant-tabs-tab {
border: none !important;
margin-left: 0px !important;
padding: 0px !important;
.nav-btns {
display: flex;
align-items: center;
justify-content: center;
}
.ant-btn-default {
border-color: transparent;
}
}
.ant-tabs-tab-active {
.nav-btns {
background: var(--bg-slate-400) !important;
}
}
.ant-tabs-nav {
margin: 0px;
margin-bottom: 0.5rem;
}
.ant-tabs-nav::before {
border-bottom: none !important;
}
.ant-tabs-nav-list {
border: 1px solid var(--bg-slate-200);
}
.ant-tabs-tab + .ant-tabs-tab {
border-left: 1px solid var(--bg-slate-200) !important;
}
.stage-run-query {
display: flex;
align-items: center;
}
}
.lightMode {
.alert-tabs {
.ant-tabs-nav-list {
border: 1px solid var(--bg-vanilla-300);
}
.ant-tabs-tab + .ant-tabs-tab {
border-left: 1px solid var(--bg-vanilla-200) !important;
}
.ant-tabs-tab-active {
.nav-btns {
background: var(--bg-vanilla-300) !important;
}
}
}
}

View File

@@ -1,7 +1,10 @@
import './QuerySection.styles.scss';
import { Button, Tabs } from 'antd';
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { QueryBuilder } from 'container/QueryBuilder';
import { Atom, LucideAccessibility, Play, Terminal } from 'lucide-react';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
@@ -49,22 +52,51 @@ function QuerySection({
const tabs = [
{
label: t('tab_qb'),
label: (
<Button className="nav-btns">
<Atom size={14} />
</Button>
),
key: EQueryType.QUERY_BUILDER,
},
{
label: t('tab_chquery'),
label: (
<Button className="nav-btns">
<Terminal size={14} />
</Button>
),
key: EQueryType.CLICKHOUSE,
},
];
const items = useMemo(
() => [
{ label: t('tab_qb'), key: EQueryType.QUERY_BUILDER },
{ label: t('tab_chquery'), key: EQueryType.CLICKHOUSE },
{ label: t('tab_promql'), key: EQueryType.PROM },
{
label: (
<Button className="nav-btns">
<Atom size={14} />
</Button>
),
key: EQueryType.QUERY_BUILDER,
},
{
label: (
<Button className="nav-btns">
<Terminal size={14} />
</Button>
),
key: EQueryType.CLICKHOUSE,
},
{
label: (
<Button className="nav-btns">
<LucideAccessibility size={14} />
</Button>
),
key: EQueryType.PROM,
},
],
[t],
[],
);
const renderTabs = (typ: AlertTypes): JSX.Element | null => {
@@ -73,40 +105,54 @@ function QuerySection({
case AlertTypes.LOGS_BASED_ALERT:
case AlertTypes.EXCEPTIONS_BASED_ALERT:
return (
<Tabs
type="card"
style={{ width: '100%' }}
defaultActiveKey={EQueryType.QUERY_BUILDER}
activeKey={queryCategory}
onChange={handleQueryCategoryChange}
tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<Button type="primary" onClick={runQuery}>
Run Query
</Button>
</span>
}
items={tabs}
/>
<div className="alert-tabs">
<Tabs
type="card"
style={{ width: '100%' }}
defaultActiveKey={EQueryType.QUERY_BUILDER}
activeKey={queryCategory}
onChange={handleQueryCategoryChange}
tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<Button
type="primary"
onClick={runQuery}
className="stage-run-query"
icon={<Play size={14} />}
>
Stage & Run Query
</Button>
</span>
}
items={tabs}
/>
</div>
);
case AlertTypes.METRICS_BASED_ALERT:
default:
return (
<Tabs
type="card"
style={{ width: '100%' }}
defaultActiveKey={EQueryType.QUERY_BUILDER}
activeKey={queryCategory}
onChange={handleQueryCategoryChange}
tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<Button type="primary" onClick={runQuery}>
Run Query
</Button>
</span>
}
items={items}
/>
<div className="alert-tabs">
<Tabs
type="card"
style={{ width: '100%' }}
defaultActiveKey={EQueryType.QUERY_BUILDER}
activeKey={queryCategory}
onChange={handleQueryCategoryChange}
tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<Button
type="primary"
onClick={runQuery}
className="stage-run-query"
icon={<Play size={14} />}
>
Stage & Run Query
</Button>
</span>
}
items={items}
/>
</div>
);
}
};
@@ -126,7 +172,7 @@ function QuerySection({
<>
<StepHeading> {t('alert_form_step1')}</StepHeading>
<FormContainer>
<div style={{ display: 'flex' }}>{renderTabs(alertType)}</div>
<div>{renderTabs(alertType)}</div>
{renderQuerySection(queryCategory)}
</FormContainer>
</>

View File

@@ -76,6 +76,10 @@ export const FormContainer = styled(Card)`
display: flex;
flex-direction: column;
border-radius: 4px;
.ant-card-body {
padding: 12px;
}
`;
export const TextareaMedium = styled(TextArea)`

View File

@@ -1,5 +1,6 @@
import { Card, Typography } from 'antd';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import ListLogView from 'components/Logs/ListLogView';
import RawLogView from 'components/Logs/RawLogView';
import Spinner from 'components/Spinner';
@@ -13,7 +14,6 @@ import { Heading } from 'container/LogsTable/styles';
import { useOptionsMenu } from 'container/OptionsMenu';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import useFontFaceObserver from 'hooks/useFontObserver';
import { useEventSource } from 'providers/EventSource';
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
@@ -51,19 +51,6 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
[logs, activeLogId],
);
useFontFaceObserver(
[
{
family: 'Fira Code',
weight: '300',
},
],
options.format === 'raw',
{
timeout: 5000,
},
);
const selectedFields = convertKeysToColumnFields(options.selectColumns);
const getItemContent = useCallback(
@@ -145,6 +132,7 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
</InfinityWrapperStyled>
)}
<LogDetail
selectedTab={VIEW_TYPES.OVERVIEW}
log={activeLog}
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}

View File

@@ -29,11 +29,11 @@ function ActionItem({
() => (
<Col>
<Button type="text" size="small" onClick={onClickHandler(OPERATORS.IN)}>
<PlusCircleOutlined /> Filter for value
<PlusCircleOutlined size={12} /> Filter for value
</Button>
<br />
<Button type="text" size="small" onClick={onClickHandler(OPERATORS.NIN)}>
<MinusCircleOutlined /> Filter out value
<MinusCircleOutlined size={12} /> Filter out value
</Button>
</Col>
),

View File

@@ -0,0 +1,3 @@
.log-context-container {
border: 1px solid var(--bg-slate-400);
}

View File

@@ -0,0 +1,54 @@
import './ContextView.styles.scss';
import RawLogView from 'components/Logs/RawLogView';
import LogsContextList from 'container/LogsContextList';
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import { ILog } from 'types/api/logs/log';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
interface LogContextProps {
log: ILog;
contextQuery: Query | undefined;
filters: TagFilter | null;
isEdit: boolean;
}
function ContextView({
log,
filters,
contextQuery,
isEdit,
}: LogContextProps): JSX.Element {
// eslint-disable-next-line react/jsx-no-useless-fragment
if (!contextQuery) return <></>;
return (
<div className="log-context-container">
<LogsContextList
className="logs-context-list-asc"
order={ORDERBY_FILTERS.ASC}
filters={filters}
isEdit={isEdit}
log={log}
query={contextQuery}
/>
<RawLogView
isActiveLog
isReadOnly
isTextOverflowEllipsisDisabled={false}
data={log}
linesPerRow={1}
/>
<LogsContextList
className="logs-context-list-desc"
order={ORDERBY_FILTERS.DESC}
filters={filters}
isEdit={isEdit}
log={log}
query={contextQuery}
/>
</div>
);
}
export default ContextView;

View File

@@ -0,0 +1,22 @@
.field-renderer-container {
display: flex !important;
gap: 8px;
align-items: center;
justify-content: space-between;
.label {
color: var(--text-robin-400);
font-family: SF Mono;
font-family: 'Space Mono', monospace;
font-size: 13px;
font-weight: var(--font-weight-normal);
line-height: 18px;
letter-spacing: -0.005em;
text-align: left;
}
.tags {
display: flex;
gap: 8;
}
}

View File

@@ -3,18 +3,23 @@ import styled from 'styled-components';
export const TagContainer = styled(Tag)`
&&& {
border-color: var(--bg-slate-400);
border-radius: 0.25rem;
padding: 0.063rem 0.5rem;
font-weight: 600;
font-size: 0.75rem;
font-size: var(--font-size-xs);
line-height: 1.25rem;
}
`;
export const TagLabel = styled.span`
font-weight: 400;
font-size: 12px;
`;
export const TagValue = styled.span`
color: var(--text-sakura-400);
/* background-color: var(--bg-slate-400); */
text-transform: capitalize;
font-size: var(--font-size-xs);
font-weight: 400;
`;

View File

@@ -1,4 +1,6 @@
import { blue } from '@ant-design/colors';
import './FieldRenderer.styles.scss';
import { Divider } from 'antd';
import { TagContainer, TagLabel, TagValue } from './FieldRenderer.styles';
import { FieldRendererProps } from './LogDetailedView.types';
@@ -8,21 +10,29 @@ function FieldRenderer({ field }: FieldRendererProps): JSX.Element {
const { dataType, newField, logType } = getFieldAttributes(field);
return (
<span>
<span className="field-renderer-container">
{dataType && newField && logType ? (
<>
<span style={{ color: blue[4] }}>{newField} </span>
<TagContainer>
<TagLabel>Type: </TagLabel>
<TagValue>{logType}</TagValue>
</TagContainer>
<TagContainer>
<TagLabel>Data type: </TagLabel>
<TagValue>{dataType}</TagValue>
</TagContainer>
<div className="label">{newField} </div>
<div className="tags">
<TagContainer>
<TagLabel>
type
<Divider type="vertical" />{' '}
</TagLabel>
<TagValue>{logType}</TagValue>
</TagContainer>
<TagContainer>
<TagLabel>
data type <Divider type="vertical" />{' '}
</TagLabel>
<TagValue>{dataType}</TagValue>
</TagContainer>
</div>
</>
) : (
<span style={{ color: blue[4] }}>{field}</span>
<span className="label">{field}</span>
)}
</span>
);

View File

@@ -0,0 +1,46 @@
.json-view-container {
border-radius: 3px;
border: 1px solid var(--bg-slate-500);
padding-top: 16px;
.json-view-footer {
height: 36px;
display: flex;
align-items: center;
border-top: 1px solid var(--bg-slate-500);
}
.log-switch {
display: flex;
justify-content: space-between;
align-items: center;
.wrap-word-switch {
display: flex;
gap: 8px;
margin-left: var(--margin-3);
align-items: center;
}
.log-switch-btn {
border: 1px solid var(--bg-slate-500);
background-color: var(--bg-slate-500);
width: 40px;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
}
}
.lightMode {
.json-view-container {
.log-switch {
.log-switch-btn {
background: var(--bg-vanilla-200);
border: 1px solid var(--bg-vanilla-400);
}
}
}
}

View File

@@ -1,44 +1,90 @@
import { blue } from '@ant-design/colors';
import { CopyFilled } from '@ant-design/icons';
import { Button, Row } from 'antd';
import Editor from 'components/Editor';
import { useMemo } from 'react';
import { useCopyToClipboard } from 'react-use';
import './JsonView.styles.scss';
import MEditor, { EditorProps, Monaco } from '@monaco-editor/react';
import { Color } from '@signozhq/design-tokens';
import { Switch, Typography } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useMemo, useState } from 'react';
import { JSONViewProps } from './LogDetailedView.types';
import { aggregateAttributesResourcesToString } from './utils';
function JSONView({ logData }: JSONViewProps): JSX.Element {
const [, copyToClipboard] = useCopyToClipboard();
const [isWrapWord, setIsWrapWord] = useState<boolean>(false);
const LogJsonData = useMemo(
() => aggregateAttributesResourcesToString(logData),
[logData],
);
const isDarkMode = useIsDarkMode();
const options: EditorProps['options'] = {
automaticLayout: true,
readOnly: true,
wordWrap: 'on',
minimap: {
enabled: false,
},
fontWeight: 400,
// fontFamily: 'SF Mono',
fontFamily: 'Space Mono',
fontSize: 13,
lineHeight: '18px',
colorDecorators: true,
scrollBeyondLastLine: false,
decorationsOverviewRuler: false,
scrollbar: {
vertical: 'hidden',
horizontal: 'hidden',
},
folding: false,
};
const handleWrapWord = (checked: boolean): void => {
setIsWrapWord(checked);
};
function setEditorTheme(monaco: Monaco): void {
monaco.editor.defineTheme('my-theme', {
base: 'vs-dark',
inherit: true,
rules: [
{ token: 'string.key.json', foreground: Color.BG_VANILLA_400 },
{ token: 'string.value.json', foreground: Color.BG_ROBIN_400 },
],
colors: {
'editor.background': Color.BG_INK_400,
},
// fontFamily: 'SF Mono',
fontFamily: 'Space Mono',
fontSize: 12,
fontWeight: 'normal',
lineHeight: 18,
letterSpacing: -0.06,
});
}
return (
<div>
<Row
style={{
justifyContent: 'flex-end',
margin: '0.5rem 0',
}}
>
<Button
size="small"
type="text"
onClick={(): void => copyToClipboard(LogJsonData)}
>
<CopyFilled /> <span style={{ color: blue[5] }}>Copy to Clipboard</span>
</Button>
</Row>
<div style={{ marginTop: '0.5rem' }}>
<Editor
value={LogJsonData}
language="json"
height="70vh"
readOnly
onChange={(): void => {}}
/>
<div className="json-view-container">
<MEditor
value={isWrapWord ? JSON.stringify(LogJsonData) : LogJsonData}
language="json"
options={options}
onChange={(): void => {}}
height="68vh"
theme={isDarkMode ? 'my-theme' : 'light'}
// eslint-disable-next-line react/jsx-no-bind
beforeMount={setEditorTheme}
/>
<div className="json-view-footer">
<div className="log-switch">
<div className="wrap-word-switch">
<Typography.Text>Wrap text</Typography.Text>
<Switch checked={isWrapWord} onChange={handleWrapWord} size="small" />
</div>
</div>
</div>
</div>
);

View File

@@ -0,0 +1,3 @@
.log-context-container {
border: 1px solid var(--bg-slate-400);
}

View File

@@ -0,0 +1,52 @@
import './LogContext.styles.scss';
import RawLogView from 'components/Logs/RawLogView';
import LogsContextList from 'container/LogsContextList';
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import { ILog } from 'types/api/logs/log';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
interface LogContextProps {
log: ILog;
contextQuery: Query | undefined;
filters: TagFilter | null;
isEdit: boolean;
}
function LogContext({
log,
filters,
contextQuery,
isEdit,
}: LogContextProps): JSX.Element {
// eslint-disable-next-line react/jsx-no-useless-fragment
if (!contextQuery) return <></>;
return (
<div className="log-context-container">
<LogsContextList
order={ORDERBY_FILTERS.ASC}
filters={filters}
isEdit={isEdit}
log={log}
query={contextQuery}
/>
<RawLogView
isActiveLog
isReadOnly
isTextOverflowEllipsisDisabled={false}
data={log}
linesPerRow={1}
/>
<LogsContextList
order={ORDERBY_FILTERS.DESC}
filters={filters}
isEdit={isEdit}
log={log}
query={contextQuery}
/>
</div>
);
}
export default LogContext;

View File

@@ -0,0 +1,131 @@
.overview-container {
.tag {
border-radius: 20px;
border: 1px solid rgba(173, 127, 88, 0.2);
background: rgba(173, 127, 88, 0.1);
padding: var(--padding-1) var(--padding-2);
font-family: 'Inter';
font-size: var(--font-size-xs);
font-weight: var(--font-weight-medium);
line-height: 16px;
letter-spacing: -0.005em;
text-align: center;
color: var(--text-sienna-400);
}
.log-switch {
height: 36px;
display: flex;
align-items: center;
border-top: 1px solid var(--bg-slate-500);
.wrap-word-switch {
display: flex;
gap: 8px;
margin-left: var(--margin-3);
align-items: center;
}
.log-switch-btn {
border: 1px solid var(--bg-slate-500);
background-color: var(--bg-slate-500);
width: 40px;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
}
.attribute-table {
margin-top: var(--margin-1);
}
.ant-collapse {
border: 1px solid var(--bg-slate-400);
}
.collapse-content {
border-bottom: 1px solid var(--bg-slate-500);
.ant-collapse-header {
align-items: center;
border-radius: 2px;
background: rgba(171, 189, 255, 0.04);
padding: 8px;
}
.ant-collapse-content {
padding: 0;
background: var(--bg-ink-400);
border-top: 1px solid var(--bg-slate-500);
.ant-collapse-content-box {
padding: var(--padding-2) 0 0 0;
}
}
}
.logs-body-content {
padding-top: 12px;
}
.ant-tag-borderless {
border-radius: 2px;
background: rgba(113, 144, 249, 0.08);
}
.attribute-collapse {
.ant-collapse-content {
.ant-collapse-content-box {
padding: 0;
}
}
}
.ant-table-wrapper .ant-table-cell {
padding: 14px 14px;
}
.attribute-tab-header {
display: flex;
align-items: center;
justify-content: space-between;
.action-btn {
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
box-shadow: none;
}
}
}
.lightMode {
.overview-container {
.ant-collapse {
border: 1px solid var(--bg-vanilla-200);
}
.collapse-content {
border-bottom: 1px solid var(--bg-vanilla-200);
.ant-collapse-content {
background: var(--bg-vanilla-100);
border-top: 1px solid var(--bg-vanilla-200);
}
}
.log-switch {
.log-switch-btn {
background: var(--bg-vanilla-200);
border: 1px solid var(--bg-vanilla-400);
}
}
}
}

View File

@@ -0,0 +1,211 @@
import './Overview.styles.scss';
import MEditor, { EditorProps, Monaco } from '@monaco-editor/react';
import { Color } from '@signozhq/design-tokens';
import {
Button,
Collapse,
Divider,
Input,
Switch,
Tag,
Typography,
} from 'antd';
import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { Search } from 'lucide-react';
import { useMemo, useState } from 'react';
import { ILog } from 'types/api/logs/log';
import { ActionItemProps } from './ActionItem';
import TableView from './TableView';
import { aggregateAttributesResourcesToString } from './utils';
interface OverviewProps {
logData: ILog;
}
type Props = OverviewProps &
Partial<Pick<ActionItemProps, 'onClickActionItem'>> &
Pick<AddToQueryHOCProps, 'onAddToQuery'>;
function Overview({
logData,
onAddToQuery,
onClickActionItem,
}: Props): JSX.Element {
const [isWrapWord, setIsWrapWord] = useState<boolean>(false);
const [isSearchVisible, setIsSearchVisible] = useState<boolean>(false);
const [isAttributesExpanded, setIsAttributesExpanded] = useState<boolean>(
true,
);
const [fieldSearchInput, setFieldSearchInput] = useState<string>('');
const logJsonData = useMemo(
() => aggregateAttributesResourcesToString(logData),
[logData],
);
const isDarkMode = useIsDarkMode();
const options: EditorProps['options'] = {
automaticLayout: true,
readOnly: true,
height: '40vh',
wordWrap: 'on',
minimap: {
enabled: false,
},
fontWeight: 400,
// fontFamily: 'SF Mono',
fontFamily: 'Space Mono',
fontSize: 13,
lineHeight: '18px',
colorDecorators: true,
scrollBeyondLastLine: false,
scrollbar: {
vertical: 'hidden',
horizontal: 'hidden',
},
};
const handleWrapWord = (checked: boolean): void => {
setIsWrapWord(checked);
};
function setEditorTheme(monaco: Monaco): void {
monaco.editor.defineTheme('my-theme', {
base: 'vs-dark',
inherit: true,
rules: [
{ token: 'string.key.json', foreground: Color.BG_VANILLA_400 },
{ token: 'string.value.json', foreground: Color.BG_ROBIN_400 },
],
colors: {
'editor.background': Color.BG_INK_400,
},
// fontFamily: 'SF Mono',
fontFamily: 'Space Mono',
fontSize: 12,
fontWeight: 'normal',
lineHeight: 18,
letterSpacing: -0.06,
});
}
const handleSearchVisible = (): void => {
setIsSearchVisible(!isSearchVisible);
};
const toogleAttributePanelOpenState = (): void => {
setIsAttributesExpanded(!isAttributesExpanded);
};
return (
<div className="overview-container">
<Collapse
defaultActiveKey={['1']}
items={[
{
key: '1',
label: (
<Tag bordered={false}>
<Typography.Text style={{ color: Color.BG_ROBIN_400 }}>
body
</Typography.Text>
</Tag>
),
children: (
<div className="logs-body-content">
<MEditor
value={isWrapWord ? JSON.stringify(logData) : logJsonData}
language={isWrapWord ? 'placetext' : 'json'}
options={options}
onChange={(): void => {}}
height="40vh"
theme={isDarkMode ? 'my-theme' : 'light'}
// eslint-disable-next-line react/jsx-no-bind
beforeMount={setEditorTheme}
/>
<Divider
style={{
margin: 0,
border: isDarkMode
? `1px solid ${Color.BG_SLATE_500}`
: `1px solid ${Color.BG_VANILLA_200}`,
}}
/>
<div className="log-switch">
<div className="wrap-word-switch">
<Typography.Text>Wrap text</Typography.Text>
<Switch checked={isWrapWord} onChange={handleWrapWord} size="small" />
</div>
</div>
</div>
),
extra: <Tag className="tag">{isWrapWord ? 'Raw' : 'JSON'}</Tag>,
className: 'collapse-content',
},
]}
/>
<Collapse
className="attribute-table"
defaultActiveKey={['1']}
bordered={false}
items={[
{
key: '1',
label: (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div
className="attribute-tab-header"
onClick={toogleAttributePanelOpenState}
>
<Tag bordered={false}>
<Typography.Text style={{ color: Color.BG_ROBIN_400 }}>
Attributes
</Typography.Text>
</Tag>
{isAttributesExpanded && (
<Button
className="action-btn"
icon={<Search size={14} />}
onClick={(e): void => {
e.stopPropagation();
handleSearchVisible();
}}
/>
)}
</div>
),
children: (
<>
{isSearchVisible && (
<Input
autoFocus
placeholder="Search for a field..."
className="search-input"
value={fieldSearchInput}
onChange={(e): void => setFieldSearchInput(e.target.value)}
/>
)}
<TableView
logData={logData}
onAddToQuery={onAddToQuery}
fieldSearchInput={fieldSearchInput}
onClickActionItem={onClickActionItem}
/>
</>
),
className: 'collapse-content attribute-collapse',
},
]}
/>
</div>
);
}
export default Overview;

View File

@@ -0,0 +1,73 @@
.attribute-table-container {
.ant-table {
background: var(--bg-ink-400);
.ant-table-row:hover {
.ant-table-cell {
.value-field {
display: flex;
justify-content: space-between;
align-items: center;
.action-btn {
display: flex;
gap: 4px;
}
}
}
}
.ant-table-cell {
border: 1px solid var(--bg-slate-500);
}
.attribute-name {
.ant-btn {
&:hover {
background-color: none !important;
}
}
}
.value-field-container {
background: rgba(22, 25, 34, 0.4);
.action-btn {
display: none;
width: max-content;
.filter-btn {
display: flex;
align-items: center;
border: none;
box-shadow: none;
border-radius: 2px;
background: var(--bg-slate-400);
height: 24px;
}
}
}
}
}
.lightMode {
.attribute-table-container {
.ant-table {
background: var(--bg-vanilla-100);
}
.ant-table-cell {
border: 1px solid var(--bg-vanilla-200);
}
.value-field-container {
background: var(--bg-vanilla-300);
.action-btn {
.filter-btn {
background: var(--bg-vanilla-300);
}
}
}
}
}

View File

@@ -1,16 +1,21 @@
import { orange } from '@ant-design/colors';
import './TableView.styles.scss';
import { LinkOutlined } from '@ant-design/icons';
import { Input, Space, Tooltip, Tree } from 'antd';
import { Color } from '@signozhq/design-tokens';
import { Button, Space, Spin, Tooltip, Tree } from 'antd';
import { ColumnsType } from 'antd/es/table';
import AddToQueryHOC, {
AddToQueryHOCProps,
} from 'components/Logs/AddToQueryHOC';
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
import { ResizeTable } from 'components/ResizeTable';
import { OPERATORS } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { fieldSearchFilter } from 'lib/logs/fieldSearch';
import { removeJSONStringifyQuotes } from 'lib/removeJSONStringifyQuotes';
import { isEmpty } from 'lodash-es';
import { ArrowDownToDot, ArrowUpFromDot } from 'lucide-react';
import { useMemo, useState } from 'react';
import { useDispatch } from 'react-redux';
import { generatePath } from 'react-router-dom';
@@ -19,7 +24,7 @@ import AppActions from 'types/actions';
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
import { ILog } from 'types/api/logs/log';
import ActionItem, { ActionItemProps } from './ActionItem';
import { ActionItemProps } from './ActionItem';
import FieldRenderer from './FieldRenderer';
import {
filterKeyForField,
@@ -34,25 +39,53 @@ const RESTRICTED_FIELDS = ['timestamp'];
interface TableViewProps {
logData: ILog;
fieldSearchInput: string;
}
type Props = TableViewProps &
Pick<AddToQueryHOCProps, 'onAddToQuery'> &
Pick<ActionItemProps, 'onClickActionItem'>;
Partial<Pick<ActionItemProps, 'onClickActionItem'>> &
Pick<AddToQueryHOCProps, 'onAddToQuery'>;
function TableView({
logData,
fieldSearchInput,
onAddToQuery,
onClickActionItem,
}: Props): JSX.Element | null {
const [fieldSearchInput, setFieldSearchInput] = useState<string>('');
const dispatch = useDispatch<Dispatch<AppActions>>();
const [isfilterInLoading, setIsFilterInLoading] = useState<boolean>(false);
const [isfilterOutLoading, setIsFilterOutLoading] = useState<boolean>(false);
const flattenLogData: Record<string, string> | null = useMemo(
() => (logData ? flattenObject(logData) : null),
[logData],
);
const handleClick = (
operator: string,
fieldKey: string,
fieldValue: string,
): void => {
const validatedFieldValue = removeJSONStringifyQuotes(fieldValue);
if (onClickActionItem) {
onClickActionItem(fieldKey, validatedFieldValue, operator);
}
};
const onClickHandler = (
operator: string,
fieldKey: string,
fieldValue: string,
) => (): void => {
handleClick(operator, fieldKey, fieldValue);
if (operator === OPERATORS.IN) {
setIsFilterInLoading(true);
}
if (operator === OPERATORS.NIN) {
setIsFilterOutLoading(true);
}
};
if (logData === null) {
return null;
}
@@ -95,24 +128,6 @@ function TableView({
}
const columns: ColumnsType<DataType> = [
{
title: 'Action',
width: 11,
render: (fieldData: Record<string, string>): JSX.Element | null => {
const fieldFilterKey = filterKeyForField(fieldData.field);
if (!RESTRICTED_FIELDS.includes(fieldFilterKey)) {
return (
<ActionItem
fieldKey={fieldFilterKey}
fieldValue={fieldData.value}
onClickActionItem={onClickActionItem}
/>
);
}
return null;
},
},
{
title: 'Field',
dataIndex: 'field',
@@ -120,6 +135,7 @@ function TableView({
width: 50,
align: 'left',
ellipsis: true,
className: 'attribute-name',
render: (field: string, record): JSX.Element => {
const renderedField = <FieldRenderer field={field} />;
@@ -127,7 +143,7 @@ function TableView({
const traceId = flattenLogData[record.field];
return (
<Space size="middle">
<Space size="middle" className="log-attribute">
{renderedField}
{traceId && (
@@ -166,15 +182,15 @@ function TableView({
},
{
title: 'Value',
dataIndex: 'value',
key: 'value',
width: 70,
ellipsis: false,
render: (field, record): JSX.Element => {
const textToCopy = field.slice(1, -1);
className: 'value-field-container attribute-value',
render: (fieldData: Record<string, string>, record): JSX.Element => {
const textToCopy = fieldData.value.slice(1, -1);
if (record.field === 'body') {
const parsedBody = recursiveParseJSON(field);
const parsedBody = recursiveParseJSON(fieldData.value);
if (!isEmpty(parsedBody)) {
return (
<Tree defaultExpandAll showLine treeData={jsonToDataNodes(parsedBody)} />
@@ -182,30 +198,58 @@ function TableView({
}
}
const fieldFilterKey = filterKeyForField(fieldData.field);
return (
<CopyClipboardHOC textToCopy={textToCopy}>
<span style={{ color: orange[6] }}>{removeEscapeCharacters(field)}</span>
</CopyClipboardHOC>
<div className="value-field">
<CopyClipboardHOC textToCopy={textToCopy}>
<span style={{ color: Color.BG_SIENNA_400 }}>
{removeEscapeCharacters(fieldData.value)}
</span>
</CopyClipboardHOC>
<span className="action-btn">
<Button
className="filter-btn"
icon={
isfilterInLoading ? (
<Spin size="small" />
) : (
<ArrowDownToDot size={14} style={{ transform: 'rotate(90deg)' }} />
)
}
onClick={onClickHandler(OPERATORS.IN, fieldFilterKey, fieldData.value)}
>
Filter for value
</Button>
<Button
className="filter-btn"
icon={
isfilterOutLoading ? (
<Spin size="small" />
) : (
<ArrowUpFromDot size={14} style={{ transform: 'rotate(90deg)' }} />
)
}
onClick={onClickHandler(OPERATORS.NIN, fieldFilterKey, fieldData.value)}
>
Filter out value
</Button>
</span>
</div>
);
},
},
];
return (
<>
<Input
placeholder="Search field names"
size="large"
value={fieldSearchInput}
onChange={(e): void => setFieldSearchInput(e.target.value)}
/>
<ResizeTable
columns={columns}
tableLayout="fixed"
dataSource={dataSource}
pagination={false}
/>
</>
<ResizeTable
columns={columns}
tableLayout="fixed"
dataSource={dataSource}
pagination={false}
showHeader={false}
className="attribute-table-container"
/>
);
}

View File

@@ -1,4 +1,5 @@
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import ROUTES from 'constants/routes';
import { getGeneratedFilterQueryString } from 'lib/getGeneratedFilterQueryString';
import getStep from 'lib/getStep';
@@ -136,6 +137,7 @@ function LogDetailedView({
return (
<LogDetail
selectedTab={VIEW_TYPES.OVERVIEW}
log={detailedLog}
onClose={onDrawerClose}
onAddToQuery={handleAddToQuery}

View File

@@ -262,3 +262,7 @@ export const removeEscapeCharacters = (str: string): string =>
};
return escapeMap[char as keyof typeof escapeMap];
});
export function removeExtraSpaces(input: string): string {
return input.replace(/\s+/g, ' ').trim();
}

View File

@@ -0,0 +1,32 @@
.qb-search-view-container {
padding: 8px 16px;
border-top: 1px solid var(--bg-slate-400, #1d212d);
border-bottom: 1px solid var(--bg-slate-400, #1d212d);
.ant-select-selector {
border-radius: 2px;
border: 1px solid var(--bg-slate-400) !important;
background-color: var(--bg-ink-300) !important;
input {
font-size: 12px;
}
.ant-tag .ant-typography {
font-size: 12px;
}
}
}
.lightMode {
.qb-search-view-container {
border-top: 1px solid var(--bg-vanilla-300);
border-bottom: 1px solid var(--bg-vanilla-300);
.ant-select-selector {
border-color: var(--bg-vanilla-300) !important;
background-color: var(--bg-vanilla-100) !important;
color: var(--bg-ink-200);
}
}
}

View File

@@ -1,4 +1,5 @@
import { Button } from 'antd';
import './LogsExplorerQuerySection.styles.scss';
import {
initialQueriesMap,
OPERATORS,
@@ -7,17 +8,26 @@ import {
import ExplorerOrderBy from 'container/ExplorerOrderBy';
import { QueryBuilder } from 'container/QueryBuilder';
import { OrderByFilterProps } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { ButtonWrapperStyled } from 'pages/LogsExplorer/styles';
import { prepareQueryWithDefaultTimestamp } from 'pages/LogsExplorer/utils';
import { memo, useCallback, useMemo } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
function LogExplorerQuerySection(): JSX.Element {
const { handleRunQuery, updateAllQueriesOperators } = useQueryBuilder();
function LogExplorerQuerySection({
selectedView,
}: {
selectedView: string;
}): JSX.Element {
const { currentQuery, updateAllQueriesOperators } = useQueryBuilder();
const query = currentQuery?.builder?.queryData[0] || null;
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
const defaultValue = useMemo(() => {
const updatedQuery = updateAllQueriesOperators(
@@ -45,6 +55,12 @@ function LogExplorerQuerySection(): JSX.Element {
return config;
}, [panelTypes]);
const { handleChangeQueryData } = useQueryOperations({
index: 0,
query,
filterConfigs,
});
const renderOrderBy = useCallback(
({ query, onChange }: OrderByFilterProps): JSX.Element => (
<ExplorerOrderBy query={query} onChange={onChange} />
@@ -59,20 +75,34 @@ function LogExplorerQuerySection(): JSX.Element {
[panelTypes, renderOrderBy],
);
const handleChangeTagFilters = useCallback(
(value: IBuilderQuery['filters']) => {
handleChangeQueryData('filters', value);
},
[handleChangeQueryData],
);
return (
<QueryBuilder
panelType={panelTypes}
config={{ initialDataSource: DataSource.LOGS, queryVariant: 'static' }}
filterConfigs={filterConfigs}
queryComponents={queryComponents}
actions={
<ButtonWrapperStyled>
<Button type="primary" onClick={handleRunQuery}>
Run Query
</Button>
</ButtonWrapperStyled>
}
/>
<>
{selectedView === 'search' && (
<div className="qb-search-view-container">
<QueryBuilderSearch
query={query}
onChange={handleChangeTagFilters}
whereClauseConfig={filterConfigs?.filters}
/>
</div>
)}
{selectedView === 'query-builder' && (
<QueryBuilder
panelType={panelTypes}
config={{ initialDataSource: DataSource.LOGS, queryVariant: 'static' }}
filterConfigs={filterConfigs}
queryComponents={queryComponents}
/>
)}
</>
);
}

View File

@@ -0,0 +1,36 @@
.context-logs-list {
position: relative;
.show-more-button {
position: absolute;
z-index: 1;
opacity: 1;
&.up {
top: 0;
}
&.down {
bottom: 0;
}
}
.virtuoso-list {
&::-webkit-scrollbar {
width: 0.1rem;
height: 0.1rem;
}
}
&.logs-context-list-asc {
.virtuoso-list {
padding-top: 16px;
}
}
&.logs-context-list-desc {
.virtuoso-list {
padding-bottom: 16px;
}
}
}

View File

@@ -0,0 +1,31 @@
.show-more-button {
background-color: var(--bg-slate-400);
color: var(--bg-vanilla-100);
display: flex;
padding: 4px 8px;
align-items: center;
gap: 3px;
border: none;
margin: 0;
}
.show-more-button {
&.disabled {
background-color: var(--bg-slate-200);
color: var(--bg-vanilla-400);
}
}
.lightMode {
.show-more-button {
background-color: var(--bg-vanilla-300);
color: var(--bg-slate-400);
}
.show-more-button {
&.disabled {
background-color: var(--bg-vanilla-300);
color: var(--bg-vanilla-400);
}
}
}

View File

@@ -1,7 +1,10 @@
import { Button, Typography } from 'antd';
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import './ShowButton.styles.scss';
import { ShowButtonWrapper } from './styles';
import { Color } from '@signozhq/design-tokens';
import { Button } from 'antd';
import cx from 'classnames';
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import { ArrowDown, ArrowUp, Ban } from 'lucide-react';
interface ShowButtonProps {
isLoading: boolean;
@@ -16,20 +19,35 @@ function ShowButton({
order,
onClick,
}: ShowButtonProps): JSX.Element {
const getIcons = (): JSX.Element => {
if (order === ORDERBY_FILTERS.ASC) {
return isDisabled ? (
<Ban size={14} style={{ color: Color.BG_VANILLA_400 }} />
) : (
<ArrowUp size={14} />
);
}
return isDisabled ? (
<Ban size={14} style={{ color: Color.BG_VANILLA_400 }} />
) : (
<ArrowDown size={14} />
);
};
return (
<ShowButtonWrapper>
<Typography>
Showing 10 lines {order === ORDERBY_FILTERS.ASC ? 'after' : 'before'} match
</Typography>
<Button
size="small"
disabled={isLoading || isDisabled}
loading={isLoading}
onClick={onClick}
>
Show 10 more lines
</Button>
</ShowButtonWrapper>
<Button
disabled={isLoading || isDisabled}
loading={isLoading}
onClick={onClick}
icon={getIcons()}
className={cx(
'show-more-button',
order === ORDERBY_FILTERS.ASC ? 'up' : 'down',
isDisabled && 'disabled',
)}
>
Load more
</Button>
);
}

View File

@@ -1,6 +1,6 @@
import { OrderByPayload } from 'types/api/queryBuilder/queryBuilderData';
export const INITIAL_PAGE_SIZE = 5;
export const INITIAL_PAGE_SIZE = 10;
export const LOGS_MORE_PAGE_SIZE = 10;
export const getOrderByTimestamp = (order: string): OrderByPayload => ({

View File

@@ -1,3 +1,5 @@
import './LogsContextList.styles.scss';
import RawLogView from 'components/Logs/RawLogView';
import Spinner from 'components/Spinner';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -21,6 +23,7 @@ import { EmptyText, ListContainer } from './styles';
import { getRequestData } from './utils';
interface LogsContextListProps {
className?: string;
isEdit: boolean;
query: Query;
log: ILog;
@@ -29,6 +32,7 @@ interface LogsContextListProps {
}
function LogsContextList({
className,
isEdit,
query,
log,
@@ -166,7 +170,7 @@ function LogsContextList({
);
return (
<>
<div className={`context-logs-list ${className}`}>
{order === ORDERBY_FILTERS.ASC && (
<ShowButton
isLoading={isFetching}
@@ -183,6 +187,7 @@ function LogsContextList({
{isFetching && <Spinner size="large" height="10rem" />}
<Virtuoso
className="virtuoso-list"
initialTopMostItemIndex={0}
data={logs}
itemContent={getItemContent}
@@ -198,8 +203,12 @@ function LogsContextList({
onClick={handleShowNextLines}
/>
)}
</>
</div>
);
}
LogsContextList.defaultProps = {
className: '',
};
export default memo(LogsContextList);

View File

@@ -1,19 +1,14 @@
import { Space, Typography } from 'antd';
import { themeColors } from 'constants/theme';
import { Color } from '@signozhq/design-tokens';
import { Typography } from 'antd';
import styled from 'styled-components';
export const ListContainer = styled.div<{ $isDarkMode: boolean }>`
position: relative;
margin: 0 -1.5rem;
height: 10rem;
overflow-y: scroll;
height: 21rem;
overflow: hidden;
background-color: ${({ $isDarkMode }): string =>
$isDarkMode ? themeColors.darkGrey : themeColors.lightgrey};
`;
export const ShowButtonWrapper = styled(Space)`
margin: 0.625rem 0;
$isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100};
`;
export const EmptyText = styled(Typography)`

View File

@@ -2,10 +2,14 @@ import { Card } from 'antd';
import styled from 'styled-components';
export const CardStyled = styled(Card)`
border: none !important;
position: relative;
margin: 0.5rem 0 3.1rem 0;
margin-bottom: 16px;
.ant-card-body {
height: 20vh;
height: 200px;
min-height: 200px;
padding: 0 12px 12px;
font-family: monospace;
}
`;

View File

@@ -1,6 +1,7 @@
import { MouseEventHandler } from 'react';
import { ILog } from 'types/api/logs/log';
export interface LogsExplorerContextProps {
log: ILog;
onClose: VoidFunction;
onClose: MouseEventHandler<HTMLElement>;
}

View File

@@ -0,0 +1,4 @@
.logs-table-row {
cursor: pointer;
position: relative;
}

View File

@@ -0,0 +1,95 @@
import './TableRow.styles.scss';
import { ColumnsType } from 'antd/es/table';
import LogLinesActionButtons from 'components/Logs/LogLinesActionButtons/LogLinesActionButtons';
import { ColumnTypeRender } from 'components/Logs/TableView/types';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useIsDarkMode } from 'hooks/useDarkMode';
import {
cloneElement,
MouseEventHandler,
ReactElement,
ReactNode,
useCallback,
useMemo,
} from 'react';
import { ILog } from 'types/api/logs/log';
import { TableCellStyled } from './styles';
interface TableRowProps {
tableColumns: ColumnsType<Record<string, unknown>>;
index: number;
log: Record<string, unknown>;
handleSetActiveContextLog: (log: ILog) => void;
logs: ILog[];
hasActions: boolean;
}
export default function TableRow({
tableColumns,
index,
log,
handleSetActiveContextLog,
logs,
hasActions,
}: TableRowProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const currentLog = useMemo(() => logs.find(({ id }) => id === log.id), [
logs,
log.id,
]);
const { onLogCopy, isLogsExplorerPage } = useCopyLogLink(currentLog?.id);
const handleShowContext: MouseEventHandler<HTMLElement> = useCallback(
(event) => {
event.preventDefault();
event.stopPropagation();
if (!handleSetActiveContextLog || !currentLog) return;
handleSetActiveContextLog(currentLog);
},
[currentLog, handleSetActiveContextLog],
);
return (
<>
{tableColumns.map((column) => {
if (!column.render) return <td>Empty</td>;
const element: ColumnTypeRender<Record<string, unknown>> = column.render(
log[column.key as keyof Record<string, unknown>],
log,
index,
);
const elementWithChildren = element as Exclude<
ColumnTypeRender<Record<string, unknown>>,
ReactNode
>;
const children = elementWithChildren.children as ReactElement;
const props = elementWithChildren.props as Record<string, unknown>;
return (
<TableCellStyled
$isDragColumn={false}
$isDarkMode={isDarkMode}
key={column.key}
>
{cloneElement(children, props)}
</TableCellStyled>
);
})}
{hasActions && isLogsExplorerPage && (
<LogLinesActionButtons
handleShowContext={handleShowContext}
onLogCopy={onLogCopy}
customClassName="table-view-log-actions"
/>
)}
</>
);
}

View File

@@ -3,4 +3,5 @@ import { CSSProperties } from 'react';
export const infinityDefaultStyles: CSSProperties = {
width: '100%',
overflowX: 'scroll',
marginTop: '15px',
};

View File

@@ -1,22 +1,13 @@
import LogDetail from 'components/LogDetail';
import { ColumnTypeRender } from 'components/Logs/TableView/types';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { useTableView } from 'components/Logs/TableView/useTableView';
import { LOCALSTORAGE } from 'constants/localStorage';
import LogsExplorerContext from 'container/LogsExplorerContext';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useDragColumns from 'hooks/useDragColumns';
import { getDraggedColumns } from 'hooks/useDragColumns/utils';
import {
cloneElement,
forwardRef,
memo,
ReactElement,
ReactNode,
useCallback,
useMemo,
} from 'react';
import { forwardRef, memo, useCallback, useMemo } from 'react';
import {
TableComponents,
TableVirtuoso,
@@ -26,11 +17,8 @@ import { ILog } from 'types/api/logs/log';
import { infinityDefaultStyles } from './config';
import { LogsCustomTable } from './LogsCustomTable';
import {
TableCellStyled,
TableHeaderCellStyled,
TableRowStyled,
} from './styles';
import { TableHeaderCellStyled, TableRowStyled } from './styles';
import TableRow from './TableRow';
import { InfinityTableProps } from './types';
// eslint-disable-next-line react/function-component-definition
@@ -64,6 +52,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
activeLog: activeContextLog,
onSetActiveLog: handleSetActiveContextLog,
onClearActiveLog: handleClearActiveContextLog,
onAddToQuery: handleAddToQuery,
} = useActiveLog();
const {
activeLog,
@@ -96,37 +85,16 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
const itemContent = useCallback(
(index: number, log: Record<string, unknown>): JSX.Element => (
<>
{tableColumns.map((column) => {
if (!column.render) return <td>Empty</td>;
const element: ColumnTypeRender<Record<string, unknown>> = column.render(
log[column.key as keyof Record<string, unknown>],
log,
index,
);
const elementWithChildren = element as Exclude<
ColumnTypeRender<Record<string, unknown>>,
ReactNode
>;
const children = elementWithChildren.children as ReactElement;
const props = elementWithChildren.props as Record<string, unknown>;
return (
<TableCellStyled
$isDragColumn={false}
$isDarkMode={isDarkMode}
key={column.key}
>
{cloneElement(children, props)}
</TableCellStyled>
);
})}
</>
<TableRow
tableColumns={tableColumns}
index={index}
log={log}
handleSetActiveContextLog={handleSetActiveContextLog}
logs={tableViewProps.logs}
hasActions
/>
),
[tableColumns, isDarkMode],
[handleSetActiveContextLog, tableColumns, tableViewProps.logs],
);
const tableHeader = useCallback(
@@ -137,13 +105,14 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
return (
<TableHeaderCellStyled
$isTimestamp={column.key === 'timestamp'}
$isDarkMode={isDarkMode}
$isDragColumn={isDragColumn}
key={column.key}
// eslint-disable-next-line react/jsx-props-no-spreading
{...(isDragColumn && { className: 'dragHandler' })}
>
{column.title as string}
{(column.title as string).replace(/^\w/, (c) => c.toUpperCase())}
</TableHeaderCellStyled>
);
})}
@@ -152,6 +121,12 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
[tableColumns, isDarkMode],
);
const handleClickExpand = (index: number): void => {
if (!onSetActiveLog) return;
onSetActiveLog(tableViewProps.logs[index]);
};
return (
<>
<TableVirtuoso
@@ -173,15 +148,21 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
{...(infitiyTableProps?.onEndReached
? { endReached: infitiyTableProps.onEndReached }
: {})}
onClick={(event: any): void => {
handleClickExpand(event.target.parentElement.parentElement.dataset.index);
}}
/>
{activeContextLog && (
<LogsExplorerContext
<LogDetail
log={activeContextLog}
onClose={handleClearActiveContextLog}
onAddToQuery={handleAddToQuery}
selectedTab={VIEW_TYPES.CONTEXT}
/>
)}
<LogDetail
selectedTab={VIEW_TYPES.OVERVIEW}
log={activeLog}
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}

View File

@@ -5,29 +5,23 @@ import { getActiveLogBackground } from 'utils/logs';
interface TableHeaderCellStyledProps {
$isDragColumn: boolean;
$isDarkMode: boolean;
$isTimestamp?: boolean;
}
export const TableStyled = styled.table`
width: 100%;
border-top: 1px solid rgba(253, 253, 253, 0.12);
border-radius: 2px 2px 0 0;
border-collapse: separate;
border-spacing: 0;
border-inline-start: 1px solid rgba(253, 253, 253, 0.12);
border-inline-end: 1px solid rgba(253, 253, 253, 0.12);
`;
export const TableCellStyled = styled.td<TableHeaderCellStyledProps>`
padding: 0.5rem;
border-inline-end: 1px solid rgba(253, 253, 253, 0.12);
border-top: 1px solid rgba(253, 253, 253, 0.12);
background-color: ${(props): string =>
props.$isDarkMode ? themeColors.black : themeColors.whiteCream};
props.$isDarkMode ? 'inherit' : themeColors.whiteCream};
color: ${(props): string =>
props.$isDarkMode ? themeColors.white : themeColors.bckgGrey};
`;
// handle the light theme here
export const TableRowStyled = styled.tr<{
$isActiveLog: boolean;
$isDarkMode: boolean;
@@ -36,34 +30,39 @@ export const TableRowStyled = styled.tr<{
${({ $isActiveLog }): string => getActiveLogBackground($isActiveLog)}
}
cursor: pointer;
position: relative;
.log-line-action-buttons {
display: none;
}
&:hover {
${TableCellStyled} {
${({ $isActiveLog, $isDarkMode }): string =>
$isActiveLog
? getActiveLogBackground()
: `background-color: ${
!$isDarkMode ? themeColors.lightgrey : themeColors.bckgGrey
};`}
!$isDarkMode ? 'var(--bg-vanilla-200)' : 'rgba(171, 189, 255, 0.04)'
}`}
}
.log-line-action-buttons {
display: flex;
}
}
`;
export const TableHeaderCellStyled = styled.th<TableHeaderCellStyledProps>`
padding: 0.5rem;
border-inline-end: 1px solid rgba(253, 253, 253, 0.12);
background-color: ${(props): string =>
!props.$isDarkMode ? themeColors.whiteCream : themeColors.bckgGrey};
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.07px;
background: ${(props): string => (props.$isDarkMode ? '#0b0c0d' : '#fdfdfd')};
${({ $isTimestamp }): string => ($isTimestamp ? 'padding-left: 24px;' : '')}
${({ $isDragColumn }): string => ($isDragColumn ? 'cursor: col-resize;' : '')}
color: ${(props): string =>
props.$isDarkMode ? themeColors.white : themeColors.bckgGrey};
&:first-child {
border-start-start-radius: 2px;
}
&:last-child {
border-start-end-radius: 2px;
border-inline-end: none;
}
props.$isDarkMode ? 'var(--bg-vanilla-100, #fff)' : themeColors.bckgGrey};
`;

View File

@@ -0,0 +1,12 @@
.logs-list-view-container {
font-family: monospace;
font-size: 14px;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.005em;
text-align: left;
// .ant-card-body {
// background-color: #0b0c0e;
// }
}

View File

@@ -1,24 +1,25 @@
import { Card, Typography } from 'antd';
import './LogsExplorerList.style.scss';
import { Card } from 'antd';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
// components
import ListLogView from 'components/Logs/ListLogView';
import RawLogView from 'components/Logs/RawLogView';
import Spinner from 'components/Spinner';
import { CARD_BODY_STYLE } from 'constants/card';
import { LOCALSTORAGE } from 'constants/localStorage';
import ExplorerControlPanel from 'container/ExplorerControlPanel';
import { Heading } from 'container/LogsTable/styles';
import { useOptionsMenu } from 'container/OptionsMenu';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useFontFaceObserver from 'hooks/useFontObserver';
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
// interfaces
import { ILog } from 'types/api/logs/log';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import NoLogs from '../NoLogs/NoLogs';
import InfinityTableView from './InfinityTableView';
import { LogsExplorerListProps } from './LogsExplorerList.interfaces';
import { InfinityWrapperStyled } from './styles';
@@ -46,7 +47,7 @@ function LogsExplorerList({
onSetActiveLog,
} = useActiveLog();
const { options, config } = useOptionsMenu({
const { options } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
dataSource: initialDataSource || DataSource.METRICS,
aggregateOperator:
@@ -58,19 +59,6 @@ function LogsExplorerList({
[logs, activeLogId],
);
useFontFaceObserver(
[
{
family: 'Fira Code',
weight: '300',
},
],
options.format === 'raw',
{
timeout: 5000,
},
);
const selectedFields = useMemo(
() => convertKeysToColumnFields(options.selectColumns),
[options],
@@ -137,7 +125,10 @@ function LogsExplorerList({
}
return (
<Card style={{ width: '100%' }} bodyStyle={CARD_BODY_STYLE}>
<Card
style={{ width: '100%', marginTop: '20px' }}
bodyStyle={CARD_BODY_STYLE}
>
<Virtuoso
ref={ref}
data={logs}
@@ -151,33 +142,23 @@ function LogsExplorerList({
}, [isLoading, options, logs, onEndReached, getItemContent, selectedFields]);
return (
<>
<ExplorerControlPanel
selectedOptionFormat={options.format}
isLoading={isLoading}
isShowPageSize={false}
optionsMenuConfig={config}
/>
<div className="logs-list-view-container">
{!isLoading && logs.length === 0 && <NoLogs />}
{options.format !== 'table' && (
<Heading>
<Typography.Text>Event</Typography.Text>
</Heading>
{!isLoading && logs.length > 0 && (
<>
<InfinityWrapperStyled>{renderContent}</InfinityWrapperStyled>
<LogDetail
selectedTab={VIEW_TYPES.OVERVIEW}
log={activeLog}
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
/>
</>
)}
{!isLoading && logs.length === 0 && (
<Typography>No logs lines found</Typography>
)}
<InfinityWrapperStyled>{renderContent}</InfinityWrapperStyled>
<LogDetail
log={activeLog}
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
/>
</>
</div>
);
}

View File

@@ -0,0 +1,58 @@
.logs-table {
.ant-table {
background: unset;
border: none;
}
.ant-table-thead {
.ant-table-cell {
background: unset !important;
border-bottom: unset !important;
color: var(--bg-vanilla-400) !important;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.07px;
}
.ant-table-cell::before {
background-color: unset !important;
}
}
.ant-table-row {
color: var(--bg-vanilla-400) !important;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400 !important;
line-height: 20px;
letter-spacing: -0.07px;
.ant-table-cell-row-hover {
background: rgba(171, 189, 255, 0.04) !important;
}
}
}
.lightMode {
.ant-table {
color: var(--bg-slate-400) !important;
}
.ant-table-thead {
.ant-table-cell {
color: var(--bg-slate-400) !important;
}
}
.ant-table-row {
color: var(--bg-slate-400) !important;
.ant-table-cell-row-hover {
background: var(--bg-vanilla-300) !important;
}
}
}

View File

@@ -1,3 +1,5 @@
import './LogsExplorerTable.styles.scss';
import { initialQueriesMap } from 'constants/queryBuilder';
import { QueryTable } from 'container/QueryTable';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
@@ -16,6 +18,7 @@ function LogsExplorerTable({
query={stagedQuery || initialQueriesMap.metrics}
queryTableData={data}
loading={isLoading}
rootClassName="logs-table"
/>
);
}

View File

@@ -0,0 +1,70 @@
.logs-explorer-views-container {
margin-bottom: 24px;
.logs-explorer-views-types {
.views-tabs-container {
padding: 8px 16px;
border: 1px solid var(--text-slate-400);
border-left: none;
border-right: none;
display: flex;
align-items: center;
justify-content: space-between;
.views-tabs {
.ant-radio-button-wrapper {
min-width: 120px;
text-align: center;
font-style: normal;
font-weight: 400;
font-size: 12px;
}
}
.tab-options {
display: flex;
gap: 8px;
align-items: center;
.ant-btn {
padding: 8px 12px;
display: flex;
align-items: center;
justify-content: center;
// border: 1px solid var(--text-slate-400);
border: none;
// background: #16181d;
// box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
.format-options-container {
position: relative;
}
}
}
.logs-explorer-views-type-content {
.ant-card {
border: none !important;
}
}
}
.ant-card-body {
background-color: var(--bg-ink-500);
}
}
.lightMode {
.logs-explorer-views-container {
.ant-card-body {
background-color: var(--bg-vanilla-100);
}
.views-tabs-container {
border: 1px solid var(--text-vanilla-300);
}
}
}

View File

@@ -1,5 +1,10 @@
import { Tabs, TabsProps } from 'antd';
import TabLabel from 'components/TabLabel';
/* eslint-disable sonarjs/cognitive-complexity */
import './LogsExplorerViews.styles.scss';
import { Button, Radio } from 'antd';
import { RadioChangeEvent } from 'antd/lib';
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
import { LOCALSTORAGE } from 'constants/localStorage';
import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes';
import { QueryParams } from 'constants/query';
import {
@@ -9,11 +14,12 @@ import {
PANEL_TYPES,
} from 'constants/queryBuilder';
import { DEFAULT_PER_PAGE_VALUE } from 'container/Controls/config';
import ExportPanel from 'container/ExportPanel';
import ExplorerOptions from 'container/ExplorerOptions/ExplorerOptions';
import GoToTop from 'container/GoToTop';
import LogsExplorerChart from 'container/LogsExplorerChart';
import LogsExplorerList from 'container/LogsExplorerList';
import LogsExplorerTable from 'container/LogsExplorerTable';
import { useOptionsMenu } from 'container/OptionsMenu';
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils';
@@ -22,10 +28,12 @@ import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useAxiosError from 'hooks/useAxiosError';
import useClickOutside from 'hooks/useClickOutside';
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData';
import { Sliders } from 'lucide-react';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
@@ -38,18 +46,27 @@ import {
Query,
TagFilter,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, LogsAggregatorOperator } from 'types/common/queryBuilder';
import {
DataSource,
LogsAggregatorOperator,
StringOperators,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
import { v4 } from 'uuid';
import { ActionsWrapper } from './LogsExplorerViews.styled';
function LogsExplorerViews(): JSX.Element {
function LogsExplorerViews({
selectedView,
showHistogram,
}: {
selectedView: string;
showHistogram: boolean;
}): JSX.Element {
const { notifications } = useNotifications();
const history = useHistory();
const { activeLogId, timeRange, onTimeRangeChange } = useCopyLogLink();
const { queryData: pageSize } = useUrlQueryData(
QueryParams.pageSize,
DEFAULT_PER_PAGE_VALUE,
@@ -63,18 +80,24 @@ function LogsExplorerViews(): JSX.Element {
// Context
const {
initialDataSource,
currentQuery,
stagedQuery,
panelType,
updateAllQueriesOperators,
} = useQueryBuilder();
const [selectedPanelType, setSelectedPanelType] = useState<PANEL_TYPES>(
panelType || PANEL_TYPES.LIST,
);
const { handleExplorerTabChange } = useHandleExplorerTabChange();
// State
const [page, setPage] = useState<number>(1);
const [logs, setLogs] = useState<ILog[]>([]);
const [requestData, setRequestData] = useState<Query | null>(null);
const [showFormatMenuItems, setShowFormatMenuItems] = useState(false);
const handleAxisError = useAxiosError();
@@ -147,6 +170,12 @@ function LogsExplorerViews(): JSX.Element {
[currentQuery, updateAllQueriesOperators],
);
const handleModeChange = (e: RadioChangeEvent): void => {
setSelectedPanelType(e.target.value);
setShowFormatMenuItems(false);
handleExplorerTabChange(e.target.value);
};
const {
data: listChartData,
isFetching: isFetchingListChartData,
@@ -155,7 +184,7 @@ function LogsExplorerViews(): JSX.Element {
enabled: !!listChartQuery && panelType === PANEL_TYPES.LIST,
});
const { data, isFetching, isError } = useGetExplorerQueryRange(
const { data, isLoading, isError } = useGetExplorerQueryRange(
requestData,
panelType,
{
@@ -327,12 +356,25 @@ function LogsExplorerViews(): JSX.Element {
);
useEffect(() => {
const shouldChangeView = isMultipleQueries || isGroupByExist;
const shouldChangeView =
(isMultipleQueries || isGroupByExist) && selectedView !== 'search';
if (panelType === PANEL_TYPES.LIST && shouldChangeView) {
if (selectedPanelType === PANEL_TYPES.LIST && shouldChangeView) {
handleExplorerTabChange(PANEL_TYPES.TIME_SERIES);
setSelectedPanelType(PANEL_TYPES.TIME_SERIES);
}
}, [panelType, isMultipleQueries, isGroupByExist, handleExplorerTabChange]);
if (panelType) {
setSelectedPanelType(panelType);
}
}, [
isMultipleQueries,
isGroupByExist,
selectedPanelType,
selectedView,
handleExplorerTabChange,
panelType,
]);
useEffect(() => {
const currentParams = data?.params as Omit<LogTimeRange, 'pageSize'>;
@@ -390,56 +432,11 @@ function LogsExplorerViews(): JSX.Element {
panelType,
]);
const tabsItems: TabsProps['items'] = useMemo(
() => [
{
label: (
<TabLabel
label="List View"
tooltipText="Please remove attributes from Group By filter to switch to List View tab"
isDisabled={isMultipleQueries || isGroupByExist}
/>
),
key: PANEL_TYPES.LIST,
disabled: isMultipleQueries || isGroupByExist,
children: (
<LogsExplorerList
isLoading={isFetching}
currentStagedQueryData={listQuery}
logs={logs}
onEndReached={handleEndReached}
/>
),
},
{
label: <TabLabel label="Time Series" isDisabled={false} />,
key: PANEL_TYPES.TIME_SERIES,
children: (
<TimeSeriesView isLoading={isFetching} data={data} isError={isError} />
),
},
{
label: 'Table',
key: PANEL_TYPES.TABLE,
children: (
<LogsExplorerTable
data={data?.payload.data.newResult.data.result || []}
isLoading={isFetching}
/>
),
},
],
[
isMultipleQueries,
isGroupByExist,
isFetching,
listQuery,
logs,
handleEndReached,
data,
isError,
],
);
const { options, config } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
dataSource: initialDataSource || DataSource.METRICS,
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
});
const chartData = useMemo(() => {
if (!stagedQuery) return [];
@@ -466,31 +463,122 @@ function LogsExplorerViews(): JSX.Element {
return isGroupByExist ? data.payload.data.result : firstPayloadQueryArray;
}, [stagedQuery, panelType, data, listChartData, listQuery]);
const formatItems = [
{
key: 'raw',
label: 'Raw',
data: {
title: 'max lines per row',
},
},
{
key: 'list',
label: 'Default',
},
{
key: 'table',
label: 'Column',
data: {
title: 'columns',
},
},
];
const handleToggleShowFormatOptions = (): void =>
setShowFormatMenuItems(!showFormatMenuItems);
const menuRef = useRef<HTMLDivElement>(null);
useClickOutside({
ref: menuRef,
onClickOutside: () => {
if (showFormatMenuItems) {
setShowFormatMenuItems(false);
}
},
});
return (
<>
<LogsExplorerChart
isLoading={isFetchingListChartData || isLoadingListChartData}
data={chartData}
/>
{stagedQuery && (
<ActionsWrapper>
<ExportPanel
query={exportDefaultQuery}
isLoading={isUpdateDashboardLoading}
onExport={handleExport}
/>
</ActionsWrapper>
<div className="logs-explorer-views-container">
{showHistogram && (
<LogsExplorerChart
isLoading={isFetchingListChartData || isLoadingListChartData}
data={chartData}
/>
)}
<Tabs
items={tabsItems}
defaultActiveKey={panelType || PANEL_TYPES.LIST}
activeKey={panelType || PANEL_TYPES.LIST}
onChange={handleExplorerTabChange}
destroyInactiveTabPane
/>
<div className="logs-explorer-views-types">
<div className="views-tabs-container">
<Radio.Group
className="views-tabs"
onChange={handleModeChange}
value={selectedPanelType}
>
<Radio.Button
value={PANEL_TYPES.LIST}
disabled={
(isMultipleQueries || isGroupByExist) && selectedView !== 'search'
}
>
List view
</Radio.Button>
<Radio.Button value={PANEL_TYPES.TIME_SERIES}> Time series </Radio.Button>
<Radio.Button value={PANEL_TYPES.TABLE}> Table </Radio.Button>
</Radio.Group>
{selectedPanelType === PANEL_TYPES.LIST && (
<div className="tab-options">
<div className="format-options-container" ref={menuRef}>
<Button onClick={handleToggleShowFormatOptions}>
<Sliders size={16} />
</Button>
{showFormatMenuItems && (
<LogsFormatOptionsMenu
title="FORMAT"
items={formatItems}
selectedOptionFormat={options.format}
config={config}
/>
)}
</div>
</div>
)}
</div>
<div className="logs-explorer-views-type-content">
{selectedPanelType === PANEL_TYPES.LIST && (
<LogsExplorerList
isLoading={isLoading}
currentStagedQueryData={listQuery}
logs={logs}
onEndReached={handleEndReached}
/>
)}
{selectedPanelType === PANEL_TYPES.TIME_SERIES && (
<TimeSeriesView isLoading={isLoading} data={data} isError={isError} />
)}
{selectedPanelType === PANEL_TYPES.TABLE && (
<LogsExplorerTable
data={data?.payload.data.newResult.data.result || []}
isLoading={isLoading}
/>
)}
</div>
</div>
<GoToTop />
</>
<ExplorerOptions
disabled={!stagedQuery}
query={exportDefaultQuery}
isLoading={isUpdateDashboardLoading}
onExport={handleExport}
sourcepage={DataSource.LOGS}
/>
</div>
);
}

View File

@@ -1,8 +1,9 @@
import { Time } from 'container/TopNav/DateTimeSelection/config';
import { Time as TimeV2 } from 'container/TopNav/DateTimeSelectionV2/config';
import { GetMinMaxPayload } from 'lib/getMinMax';
export const getGlobalTime = (
selectedTime: Time,
selectedTime: Time | TimeV2,
globalTime: GetMinMaxPayload,
): GetMinMaxPayload | undefined => {
if (selectedTime === 'custom') {

View File

@@ -2,6 +2,7 @@ import './logsTable.styles.scss';
import { Card, Typography } from 'antd';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
// components
import ListLogView from 'components/Logs/ListLogView';
import RawLogView from 'components/Logs/RawLogView';
@@ -9,7 +10,6 @@ import LogsTableView from 'components/Logs/TableView';
import Spinner from 'components/Spinner';
import { CARD_BODY_STYLE } from 'constants/card';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import useFontFaceObserver from 'hooks/useFontObserver';
import { memo, useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { Virtuoso } from 'react-virtuoso';
@@ -37,19 +37,6 @@ function LogsTable(props: LogsTableProps): JSX.Element {
onSetActiveLog,
} = useActiveLog();
useFontFaceObserver(
[
{
family: 'Fira Code',
weight: '300',
},
],
viewMode === 'raw',
{
timeout: 5000,
},
);
const {
logs,
fields: { selected },
@@ -125,6 +112,7 @@ function LogsTable(props: LogsTableProps): JSX.Element {
{renderContent}
<LogDetail
selectedTab={VIEW_TYPES.OVERVIEW}
log={activeLog}
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}

View File

@@ -1,3 +1,3 @@
.logs-card {
flex: 1;
}
flex: 1;
}

View File

@@ -3,3 +3,9 @@
align-items: center;
gap: 8px;
}
.theme-option {
display: flex;
align-items: center;
gap: 8px;
}

View File

@@ -4,7 +4,7 @@ import './UserInfo.styles.scss';
import { Button, Card, Flex, Input, Space, Typography } from 'antd';
import editUser from 'api/user/editUser';
import { useNotifications } from 'hooks/useNotifications';
import { PencilIcon, UserSquare } from 'lucide-react';
import { PencilIcon } from 'lucide-react';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
@@ -79,7 +79,6 @@ function UserInfo(): JSX.Element {
<Card>
<Space direction="vertical" size="middle">
<Flex gap={8}>
<UserSquare />{' '}
<Typography.Title level={4} style={{ marginTop: 0 }}>
User Details
</Typography.Title>

View File

@@ -1,13 +1,44 @@
import './MySettings.styles.scss';
import { Button, Space } from 'antd';
import { Button, Radio, RadioChangeEvent, Space, Typography } from 'antd';
import { Logout } from 'api/utils';
import { LogOut } from 'lucide-react';
import useThemeMode, { useIsDarkMode } from 'hooks/useDarkMode';
import { LogOut, Moon, Sun } from 'lucide-react';
import { useState } from 'react';
import Password from './Password';
import UserInfo from './UserInfo';
function MySettings(): JSX.Element {
const isDarkMode = useIsDarkMode();
const { toggleTheme } = useThemeMode();
const themeOptions = [
{
label: (
<div className="theme-option">
<Moon size={12} /> Dark{' '}
</div>
),
value: 'dark',
},
{
label: (
<div className="theme-option">
<Sun size={12} /> Light{' '}
</div>
),
value: 'light',
},
];
const [theme, setTheme] = useState(isDarkMode ? 'dark' : 'light');
const handleThemeChange = ({ target: { value } }: RadioChangeEvent): void => {
setTheme(value);
toggleTheme();
};
return (
<Space
direction="vertical"
@@ -16,9 +47,32 @@ function MySettings(): JSX.Element {
margin: '16px 0',
}}
>
<UserInfo />
<div className="theme-selector">
<Typography.Title
level={5}
style={{
margin: '0 0 16px 0',
}}
>
{' '}
Theme{' '}
</Typography.Title>
<Radio.Group
options={themeOptions}
onChange={handleThemeChange}
value={theme}
optionType="button"
buttonStyle="solid"
/>
</div>
<Password />
<div className="user-info-container">
<UserInfo />
</div>
<div className="password-reset-container">
<Password />
</div>
<Button className="flexBtn" onClick={(): void => Logout()} type="primary">
<LogOut size={12} /> Logout

View File

@@ -48,7 +48,7 @@ function DashboardDescription(): JSX.Element {
};
return (
<Card>
<Card style={{ marginTop: '1rem' }}>
<Row gutter={16}>
<Col flex={1} span={9}>
<Typography.Title

View File

@@ -30,11 +30,10 @@ function QueryHeader({
const [collapse, setCollapse] = useState(false);
return (
<QueryWrapper>
<Row style={{ justifyContent: 'space-between' }}>
<Row style={{ justifyContent: 'space-between', marginBottom: '1rem' }}>
<Row>
<Button
type="default"
ghost
icon={disabled ? <EyeInvisibleFilled /> : <EyeFilled />}
onClick={onDisable}
>
@@ -42,7 +41,6 @@ function QueryHeader({
</Button>
<Button
type="default"
ghost
icon={collapse ? <RightOutlined /> : <DownOutlined />}
onClick={(): void => setCollapse(!collapse)}
/>
@@ -51,7 +49,6 @@ function QueryHeader({
{deletable && (
<Button
type="default"
ghost
danger
icon={<DeleteOutlined />}
onClick={onDelete}

View File

@@ -0,0 +1,55 @@
.dashboard-navigation {
.ant-tabs-tab {
border: none !important;
margin-left: 0px !important;
padding: 0px !important;
.nav-btns {
display: flex;
align-items: center;
justify-content: center;
}
.ant-btn-default {
border-color: transparent;
}
}
.ant-tabs-tab-active {
.nav-btns {
background: var(--bg-slate-400) !important;
}
}
.ant-tabs-nav {
margin: 0px;
margin-bottom: 0.5rem;
}
.ant-tabs-nav::before {
border-bottom: none !important;
}
.ant-tabs-nav-list {
border: 1px solid var(--bg-slate-200);
}
.ant-tabs-tab + .ant-tabs-tab {
border-left: 1px solid var(--bg-slate-200) !important;
}
.stage-run-query {
display: flex;
align-items: center;
}
}
.lightMode {
.dashboard-navigation {
.ant-tabs-nav-list {
border: 1px solid var(--bg-vanilla-300);
}
.ant-tabs-tab + .ant-tabs-tab {
border-left: 1px solid var(--bg-vanilla-200) !important;
}
.ant-tabs-tab-active {
.nav-btns {
background: var(--bg-vanilla-300) !important;
}
}
}
}

View File

@@ -1,3 +1,5 @@
import './QuerySection.styles.scss';
import { Button, Tabs, Typography } from 'antd';
import TextToolTip from 'components/TextToolTip';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -9,6 +11,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
import useUrlQuery from 'hooks/useUrlQuery';
import { Atom, LucideAccessibility, Play, Terminal } from 'lucide-react';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import {
getNextWidgets,
@@ -132,7 +135,11 @@ function QuerySection({
const items = [
{
key: EQueryType.QUERY_BUILDER,
label: 'Query Builder',
label: (
<Button className="nav-btns">
<Atom size={14} />
</Button>
),
tab: <Typography>Query Builder</Typography>,
children: (
<QueryBuilder panelType={selectedGraph} filterConfigs={filterConfigs} />
@@ -140,39 +147,51 @@ function QuerySection({
},
{
key: EQueryType.CLICKHOUSE,
label: 'ClickHouse Query',
label: (
<Button className="nav-btns">
<Terminal size={14} />
</Button>
),
tab: <Typography>ClickHouse Query</Typography>,
children: <ClickHouseQueryContainer />,
},
{
key: EQueryType.PROM,
label: 'PromQL',
label: (
<Button className="nav-btns">
<LucideAccessibility size={14} />
</Button>
),
tab: <Typography>PromQL</Typography>,
children: <PromQLQueryContainer />,
},
];
return (
<Tabs
type="card"
style={{ width: '100%' }}
defaultActiveKey={currentQuery.queryType}
activeKey={currentQuery.queryType}
onChange={handleQueryCategoryChange}
tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<TextToolTip text="This will temporarily save the current query and graph state. This will persist across tab change" />
<Button
loading={getWidgetQueryRange.isFetching}
type="primary"
onClick={handleRunQuery}
>
Stage & Run Query
</Button>
</span>
}
items={items}
/>
<div className="dashboard-navigation">
<Tabs
type="card"
style={{ width: '100%' }}
defaultActiveKey={currentQuery.queryType}
activeKey={currentQuery.queryType}
onChange={handleQueryCategoryChange}
tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<TextToolTip text="This will temporarily save the current query and graph state. This will persist across tab change" />
<Button
loading={getWidgetQueryRange.isFetching}
type="primary"
onClick={handleRunQuery}
className="stage-run-query"
icon={<Play size={14} />}
>
Stage & Run Query
</Button>
</span>
}
items={items}
/>
</div>
);
}

View File

@@ -6,4 +6,8 @@ export const QueryContainer = styled(Card)`
margin-top: 1rem;
min-height: 23.5%;
}
.ant-card-body {
padding: 12px;
}
`;

View File

@@ -0,0 +1,44 @@
.no-logs-container {
height: 400px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
// border: 1px solid #1d212d;
border-radius: 3px;
.no-logs-container-content {
display: flex;
flex-direction: column;
gap: 8px;
.eyes-emoji {
height: 32px;
width: 32px;
}
.no-logs-text {
font-size: 14px;
font-weight: 500;
.sub-text {
font-weight: 400;
color: #c0c1c3;
}
}
.send-logs-link {
display: flex;
align-items: center;
gap: 8px;
color: #7190f9;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
}
}
}

View File

@@ -0,0 +1,24 @@
import './NoLogs.styles.scss';
import { Typography } from 'antd';
import { ArrowUpRight } from 'lucide-react';
export default function NoLogs(): JSX.Element {
return (
<div className="no-logs-container">
<div className="no-logs-container-content">
<img className="eyes-emoji" src="/Images/eyesEmoji.svg" alt="eyes emoji" />
<Typography className="no-logs-text">
No logs yet.{' '}
<span className="sub-text">
When we receive logs, they would show up here
</span>
</Typography>
<Typography.Link className="send-logs-link">
Sending Logs to SigNoz <ArrowUpRight size={16} />
</Typography.Link>
</div>
</div>
);
}

View File

@@ -2,6 +2,7 @@ import './styles.scss';
import { ExpandAltOutlined } from '@ant-design/icons';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import dayjs from 'dayjs';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { ILog } from 'types/api/logs/log';
@@ -36,6 +37,7 @@ function LogsList({ logs }: LogsListProps): JSX.Element {
</div>
))}
<LogDetail
selectedTab={VIEW_TYPES.OVERVIEW}
log={activeLog}
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}

View File

@@ -0,0 +1,221 @@
.query-builder-container {
position: relative;
border-top: 1px solid var(--bg-slate-400);
border-bottom: 1px solid var(--bg-slate-400);
background: var(--bg-ink-400);
// height: 280px;
// overflow: hidden;
.query-builder-left-col {
// min-height: 280px;
// position: relative;
border-top: 1px solid var(--bg-slate-400);
border-right: 1px solid var(--bg-slate-400);
border-bottom: 1px solid var(--bg-slate-400);
border-left: 1px solid var(--bg-slate-400);
background: var(--bg-slate-500);
}
.new-query-formula-buttons-container {
position: absolute;
display: flex;
z-index: 10;
bottom: 10px;
left: 16px;
border-radius: 2px;
border: 1px solid var(--bg-slate-200);
background: var(--bg-ink-200);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
.ant-btn {
border-radius: 0px;
border: 1px solid var(--bg-slate-200);
background: var(--bg-ink-200);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
padding: 2px 8px;
display: flex;
justify-content: center;
align-items: center;
}
}
.query-builder-mini-map {
border-left: 1px solid var(--bg-slate-400);
border-right: 1px solid var(--bg-slate-400);
display: flex;
flex-direction: column;
justify-content: flex-start;
gap: 8px;
align-items: center;
padding: 8px;
// min-height: 280px;
// height: 100%;
// overflow-y: auto;
.ant-btn {
min-width: 32px;
padding: 4px 9px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 2px;
&.query-btn {
color: var(--bg-sakura-400);
border: 1px solid rgba(242, 71, 105, 0.2);
background: rgba(242, 71, 105, 0.1);
&:hover {
border: 1px solid rgba(242, 71, 105, 0.4);
color: var(--bg-sakura-400);
}
}
&.formula-btn {
color: var(--bg-sienna-400);
border: 1px solid rgba(189, 153, 121, 0.2);
background: rgba(189, 153, 121, 0.1);
&:hover {
border: 1px solid rgba(189, 153, 121, 0.4);
color: var(--bg-sienna-400);
}
}
}
}
.qb-entities-list {
// height: 280px;
// overflow-y: auto;
// height: 100%;
scroll-behavior: smooth;
}
.query-builder {
padding: 12px;
// height: 348px;
// overflow-y: auto;
// scroll-snap-align: start;
.query-builder-queries-formula-container {
display: flex;
gap: 16px;
// height: 100%;
scroll-snap-type: y mandatory;
.query,
.formula {
scroll-snap-align: start;
}
}
.ant-select-selector {
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
input {
font-size: 12px;
}
.ant-tag .ant-typography {
font-size: 12px;
}
}
.ant-input-number {
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
}
.divider {
margin: 12px -12px;
.ant-divider-horizontal {
width: calc(100% + 24px);
margin: 24px 0 12px;
border-block-start: 2px solid var(--bg-slate-400);
}
}
}
::-webkit-scrollbar {
height: 1rem;
width: 0.2rem;
}
}
.lightMode {
.query-builder-container {
border-top-color: var(--bg-vanilla-300);
border-bottom-color: var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
.query-builder-left-col {
border-top-color: var(--bg-vanilla-300);
border-right-color: var(--bg-vanilla-300);
border-bottom-color: var(--bg-vanilla-300);
border-left-color: var(--bg-vanilla-300);
background-color: var(--bg-vanilla-100);
.new-query-formula-buttons-container {
border-color: var(--bg-vanilla-200);
background: var(--bg-vanilla-200);
.ant-btn {
border-color: var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
}
}
}
.new-query-formula-buttons-container {
border-color: var(--bg-vanilla-200);
background: var(--bg-vanilla-200);
.ant-btn {
border-color: var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
}
}
.query-builder-mini-map {
border-left-color: var(--bg-vanilla-300);
border-right-color: var(--bg-vanilla-300);
}
.query-builder {
.divider {
.ant-divider-horizontal {
border-block-start: 2px solid var(--bg-vanilla-300);
}
}
.ant-select-selector {
border-color: var(--bg-vanilla-300);
background: var(--bg-vanilla-300);
}
.ant-input-number {
border-color: var(--bg-vanilla-300);
}
.anticon {
color: var(--bg-vanilla-400);
}
}
}
}

View File

@@ -1,10 +1,12 @@
import { PlusOutlined } from '@ant-design/icons';
import { Button, Col, Row } from 'antd';
import './QueryBuilder.styles.scss';
import { Button, Col, Divider, Row } from 'antd';
import { MAX_FORMULAS, MAX_QUERIES } from 'constants/queryBuilder';
// ** Hooks
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { DatabaseZap, Sigma } from 'lucide-react';
// ** Constants
import { memo, useEffect, useMemo } from 'react';
import { memo, useEffect, useMemo, useRef } from 'react';
import { DataSource } from 'types/common/queryBuilder';
// ** Components
@@ -15,7 +17,6 @@ import { QueryBuilderProps } from './QueryBuilder.interfaces';
export const QueryBuilder = memo(function QueryBuilder({
config,
panelType: newPanelType,
actions,
filterConfigs = {},
queryComponents,
}: QueryBuilderProps): JSX.Element {
@@ -28,6 +29,8 @@ export const QueryBuilder = memo(function QueryBuilder({
initialDataSource,
} = useQueryBuilder();
const containerRef = useRef(null);
const currentDataSource = useMemo(
() =>
(config && config.queryVariant === 'static' && config.initialDataSource) ||
@@ -64,70 +67,126 @@ export const QueryBuilder = memo(function QueryBuilder({
[currentQuery],
);
const handleScrollIntoView = (
entityType: string,
entityName: string,
): void => {
const selectedEntity = document.getElementById(
`qb-${entityType}-${entityName}`,
);
if (selectedEntity) {
selectedEntity.scrollIntoView({
behavior: 'smooth',
block: 'start',
inline: 'nearest',
});
}
};
return (
<Row style={{ width: '100%' }} gutter={[0, 20]} justify="start">
<Col span={24}>
<Row gutter={[0, 50]}>
{currentQuery.builder.queryData.map((query, index) => (
<Col key={query.queryName} span={24}>
<Query
index={index}
isAvailableToDisable={isAvailableToDisableQuery}
queryVariant={config?.queryVariant || 'dropdown'}
query={query}
filterConfigs={filterConfigs}
queryComponents={queryComponents}
/>
<Row
style={{ width: '100%' }}
gutter={[0, 20]}
justify="start"
className="query-builder-container"
>
<div className="new-query-formula-buttons-container">
<Button disabled={isDisabledQueryButton} onClick={addNewBuilderQuery}>
<DatabaseZap size={12} />
</Button>
<Button disabled={isDisabledFormulaButton} onClick={addNewFormula}>
<Sigma size={12} />
</Button>
</div>
<Col span={23} className="qb-entities-list">
<Row>
<Col span={1} className="query-builder-left-col">
{' '}
</Col>
<Col span={23} className="query-builder">
<Row
gutter={[0, 16]}
className="query-builder-queries-formula-container"
ref={containerRef}
>
{currentQuery.builder.queryData.map((query, index) => (
<Col
key={query.queryName}
span={24}
className="query"
id={`qb-query-${query.queryName}`}
>
<Query
index={index}
isAvailableToDisable={isAvailableToDisableQuery}
queryVariant={config?.queryVariant || 'dropdown'}
query={query}
filterConfigs={filterConfigs}
queryComponents={queryComponents}
/>
</Col>
))}
{currentQuery.builder.queryFormulas.map((formula, index) => {
const isAllMetricDataSource = currentQuery.builder.queryData.every(
(query) => query.dataSource === DataSource.METRICS,
);
const query =
currentQuery.builder.queryData[index] ||
currentQuery.builder.queryData[0];
return (
<Col
key={formula.queryName}
span={24}
className="formula"
id={`qb-formula-${formula.queryName}`}
>
<Formula
filterConfigs={filterConfigs}
query={query}
isAdditionalFilterEnable={isAllMetricDataSource}
formula={formula}
index={index}
/>
</Col>
);
})}
</Row>
<Col span={24} className="divider">
<Divider />
</Col>
))}
{currentQuery.builder.queryFormulas.map((formula, index) => {
const isAllMetricDataSource = currentQuery.builder.queryData.every(
(query) => query.dataSource === DataSource.METRICS,
);
const query =
currentQuery.builder.queryData[index] ||
currentQuery.builder.queryData[0];
return (
<Col key={formula.queryName} span={24}>
<Formula
filterConfigs={filterConfigs}
query={query}
isAdditionalFilterEnable={isAllMetricDataSource}
formula={formula}
index={index}
/>
</Col>
);
})}
</Col>
</Row>
</Col>
<Col span={24}>
<Row gutter={[20, 0]}>
<Col>
<Button
disabled={isDisabledQueryButton}
type="primary"
icon={<PlusOutlined />}
onClick={addNewBuilderQuery}
>
Query
</Button>
</Col>
<Col>
<Button
disabled={isDisabledFormulaButton}
onClick={addNewFormula}
type="primary"
icon={<PlusOutlined />}
>
Formula
</Button>
</Col>
{actions}
</Row>
<Col span={1} className="query-builder-mini-map">
{currentQuery.builder.queryData.map((query) => (
<Button
disabled={isDisabledQueryButton}
className="query-btn"
key={query.queryName}
onClick={(): void => handleScrollIntoView('query', query.queryName)}
>
{query.queryName}
</Button>
))}
{currentQuery.builder.queryFormulas.map((formula) => (
<Button
disabled={isDisabledFormulaButton}
className="formula-btn"
key={formula.queryName}
onClick={(): void => handleScrollIntoView('formula', formula.queryName)}
>
{formula.queryName}
</Button>
))}
</Col>
</Row>
);

View File

@@ -19,6 +19,7 @@ export const StyledInner = styled(Col)`
width: fit-content;
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 0.875rem;
min-height: 1.375rem;
cursor: pointer;

View File

@@ -0,0 +1,3 @@
.filter-toggler {
margin-right: 8px;
}

View File

@@ -1,15 +1,11 @@
import { Col, Row, Typography } from 'antd';
import { MinusSquare, PlusSquare } from 'lucide-react';
import { Fragment, memo, ReactNode, useState } from 'react';
// ** Types
import { AdditionalFiltersProps } from './AdditionalFiltersToggler.interfaces';
// ** Styles
import {
StyledIconClose,
StyledIconOpen,
StyledInner,
StyledLink,
} from './AdditionalFiltersToggler.styled';
import { StyledInner, StyledLink } from './AdditionalFiltersToggler.styled';
export const AdditionalFiltersToggler = memo(function AdditionalFiltersToggler({
children,
@@ -44,8 +40,13 @@ export const AdditionalFiltersToggler = memo(function AdditionalFiltersToggler({
return (
<Row>
<Col span={24}>
<StyledInner onClick={handleToggleOpenFilters}>
{isOpenedFilters ? <StyledIconClose /> : <StyledIconOpen />}
<StyledInner onClick={handleToggleOpenFilters} style={{ marginBottom: 0 }}>
{isOpenedFilters ? (
<MinusSquare size={14} fill="#4E74F8" />
) : (
<PlusSquare size={14} fill="#4E74F8" />
)}
{!isOpenedFilters && (
<Typography>Add conditions for {filtersTexts}</Typography>
)}

Some files were not shown because too many files have changed in this diff Show More