Compare commits
27 Commits
main
...
logs-detai
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3608111de | ||
|
|
e0c2000e00 | ||
|
|
28b97d3b5a | ||
|
|
bbbacdeb6d | ||
|
|
4bcab49f1f | ||
|
|
dc48d46e26 | ||
|
|
aa63fd541b | ||
|
|
9a40cb72cf | ||
|
|
a128b0635a | ||
|
|
e6495532bc | ||
|
|
194b15db79 | ||
|
|
d4bea0df35 | ||
|
|
f4234ccf46 | ||
|
|
89f6d08316 | ||
|
|
5f9329c8d1 | ||
|
|
07ce1f520d | ||
|
|
735e55f506 | ||
|
|
023fb93b75 | ||
|
|
0ddeceb2ab | ||
|
|
a772d4cf54 | ||
|
|
7c97be51d6 | ||
|
|
beb63c0ce5 | ||
|
|
45fe4b1779 | ||
|
|
a025ce54d2 | ||
|
|
e7c8eae7c4 | ||
|
|
6ff7d9dfb4 | ||
|
|
abce08c851 |
@@ -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",
|
||||
|
||||
19
frontend/public/Images/eyesEmoji.svg
Normal file
19
frontend/public/Images/eyesEmoji.svg
Normal 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 |
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
310
frontend/src/components/CustomTimePicker/CustomTimePicker.tsx
Normal file
310
frontend/src/components/CustomTimePicker/CustomTimePicker.tsx
Normal 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,
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)`
|
||||
|
||||
@@ -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'>;
|
||||
|
||||
224
frontend/src/components/LogDetail/LogDetails.styles.scss
Normal file
224
frontend/src/components/LogDetail/LogDetails.styles.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
7
frontend/src/components/LogDetail/constants.ts
Normal file
7
frontend/src/components/LogDetail/constants.ts
Normal 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];
|
||||
@@ -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"> </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>
|
||||
);
|
||||
}
|
||||
|
||||
103
frontend/src/components/Logs/ListLogView/ListLogView.styles.scss
Normal file
103
frontend/src/components/Logs/ListLogView/ListLogView.styles.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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: '',
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)`
|
||||
|
||||
@@ -12,5 +12,6 @@ export interface RawLogContentProps {
|
||||
linesPerRow: number;
|
||||
$isReadOnly?: boolean;
|
||||
$isActiveLog?: boolean;
|
||||
$isDarkMode?: boolean;
|
||||
$isTextOverflowEllipsisDisabled?: boolean;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
`;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
233
frontend/src/container/ExplorerOptions/ExplorerOptions.tsx
Normal file
233
frontend/src/container/ExplorerOptions/ExplorerOptions.tsx
Normal 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;
|
||||
8
frontend/src/container/ExplorerOptions/constants.ts
Normal file
8
frontend/src/container/ExplorerOptions/constants.ts
Normal 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,
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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)`
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.log-context-container {
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
`;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
46
frontend/src/container/LogDetailedView/JsonView.styles.scss
Normal file
46
frontend/src/container/LogDetailedView/JsonView.styles.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.log-context-container {
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
}
|
||||
52
frontend/src/container/LogDetailedView/LogContext.tsx
Normal file
52
frontend/src/container/LogDetailedView/LogContext.tsx
Normal 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;
|
||||
131
frontend/src/container/LogDetailedView/Overview.styles.scss
Normal file
131
frontend/src/container/LogDetailedView/Overview.styles.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
211
frontend/src/container/LogDetailedView/Overview.tsx
Normal file
211
frontend/src/container/LogDetailedView/Overview.tsx
Normal 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;
|
||||
73
frontend/src/container/LogDetailedView/TableView.styles.scss
Normal file
73
frontend/src/container/LogDetailedView/TableView.styles.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 => ({
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)`
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
.logs-table-row {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
@@ -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"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -3,4 +3,5 @@ import { CSSProperties } from 'react';
|
||||
export const infinityDefaultStyles: CSSProperties = {
|
||||
width: '100%',
|
||||
overflowX: 'scroll',
|
||||
marginTop: '15px',
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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};
|
||||
`;
|
||||
|
||||
@@ -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;
|
||||
// }
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
.logs-card {
|
||||
flex: 1;
|
||||
}
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@@ -3,3 +3,9 @@
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.theme-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -48,7 +48,7 @@ function DashboardDescription(): JSX.Element {
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card style={{ marginTop: '1rem' }}>
|
||||
<Row gutter={16}>
|
||||
<Col flex={1} span={9}>
|
||||
<Typography.Title
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,4 +6,8 @@ export const QueryContainer = styled(Card)`
|
||||
margin-top: 1rem;
|
||||
min-height: 23.5%;
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
padding: 12px;
|
||||
}
|
||||
`;
|
||||
|
||||
44
frontend/src/container/NoLogs/NoLogs.styles.scss
Normal file
44
frontend/src/container/NoLogs/NoLogs.styles.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
24
frontend/src/container/NoLogs/NoLogs.tsx
Normal file
24
frontend/src/container/NoLogs/NoLogs.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
221
frontend/src/container/QueryBuilder/QueryBuilder.styles.scss
Normal file
221
frontend/src/container/QueryBuilder/QueryBuilder.styles.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.filter-toggler {
|
||||
margin-right: 8px;
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user