Compare commits
27 Commits
demo/trace
...
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/loader": "2.3.0",
|
||||||
"@mdx-js/react": "2.3.0",
|
"@mdx-js/react": "2.3.0",
|
||||||
"@monaco-editor/react": "^4.3.1",
|
"@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",
|
"@uiw/react-md-editor": "3.23.5",
|
||||||
"@xstate/react": "^3.0.0",
|
"@xstate/react": "^3.0.0",
|
||||||
"ansi-to-html": "0.7.2",
|
"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';
|
} from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
|
||||||
Col,
|
Col,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
MenuProps,
|
MenuProps,
|
||||||
@@ -152,95 +151,100 @@ function ExplorerCard({
|
|||||||
const saveButtonType = isQueryUpdated ? 'default' : 'primary';
|
const saveButtonType = isQueryUpdated ? 'default' : 'primary';
|
||||||
const saveButtonIcon = isQueryUpdated ? null : <SaveOutlined />;
|
const saveButtonIcon = isQueryUpdated ? null : <SaveOutlined />;
|
||||||
|
|
||||||
|
const showSaveView = false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ExplorerCardHeadContainer size="small">
|
{showSaveView && (
|
||||||
<Row align="middle">
|
<ExplorerCardHeadContainer size="small">
|
||||||
<Col span={6}>
|
<Row align="middle">
|
||||||
<Space>
|
<Col span={6}>
|
||||||
<Typography>Query Builder</Typography>
|
<Space>
|
||||||
<TextToolTip
|
<Typography>Query Builder</Typography>
|
||||||
url={ExploreHeaderToolTip.url}
|
<TextToolTip
|
||||||
text={ExploreHeaderToolTip.text}
|
url={ExploreHeaderToolTip.url}
|
||||||
useFilledIcon={false}
|
text={ExploreHeaderToolTip.text}
|
||||||
/>
|
useFilledIcon={false}
|
||||||
</Space>
|
/>
|
||||||
</Col>
|
</Space>
|
||||||
<OffSetCol span={18}>
|
</Col>
|
||||||
<Space size="large">
|
<OffSetCol span={18}>
|
||||||
{viewsData?.data.data && viewsData?.data.data.length && (
|
<Space size="large">
|
||||||
<Space>
|
{viewsData?.data.data && viewsData?.data.data.length && (
|
||||||
<Select
|
<Space>
|
||||||
getPopupContainer={popupContainer}
|
<Select
|
||||||
loading={isLoading || isRefetching}
|
getPopupContainer={popupContainer}
|
||||||
showSearch
|
loading={isLoading || isRefetching}
|
||||||
placeholder="Select a view"
|
showSearch
|
||||||
dropdownStyle={DropDownOverlay}
|
placeholder="Select a view"
|
||||||
dropdownMatchSelectWidth={false}
|
dropdownStyle={DropDownOverlay}
|
||||||
optionLabelProp="value"
|
dropdownMatchSelectWidth={false}
|
||||||
value={viewName || undefined}
|
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) => (
|
Save changes
|
||||||
<Select.Option key={view.uuid} value={view.name}>
|
</Button>
|
||||||
<MenuItemGenerator
|
)}
|
||||||
viewName={view.name}
|
<Popover
|
||||||
viewKey={viewKey}
|
getPopupContainer={popupContainer}
|
||||||
createdBy={view.createdBy}
|
placement="bottomLeft"
|
||||||
uuid={view.uuid}
|
trigger="click"
|
||||||
refetchAllView={refetchAllView}
|
content={
|
||||||
viewData={viewsData.data.data}
|
<SaveViewWithName
|
||||||
sourcePage={sourcepage}
|
sourcePage={sourcepage}
|
||||||
/>
|
handlePopOverClose={handleOpenChange}
|
||||||
</Select.Option>
|
refetchAllView={refetchAllView}
|
||||||
))}
|
/>
|
||||||
</Select>
|
}
|
||||||
</Space>
|
showArrow={false}
|
||||||
)}
|
open={isOpen}
|
||||||
{isQueryUpdated && (
|
onOpenChange={handleOpenChange}
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
icon={<SaveOutlined />}
|
|
||||||
onClick={onUpdateQueryHandler}
|
|
||||||
>
|
>
|
||||||
Save changes
|
<Button
|
||||||
</Button>
|
type={saveButtonType}
|
||||||
)}
|
icon={saveButtonIcon}
|
||||||
<Popover
|
data-testid="traces-save-view-action"
|
||||||
getPopupContainer={popupContainer}
|
>
|
||||||
placement="bottomLeft"
|
{isQueryUpdated
|
||||||
trigger="click"
|
? SaveButtonText.SAVE_AS_NEW_VIEW
|
||||||
content={
|
: SaveButtonText.SAVE_VIEW}
|
||||||
<SaveViewWithName
|
</Button>
|
||||||
sourcePage={sourcepage}
|
</Popover>
|
||||||
handlePopOverClose={handleOpenChange}
|
<ShareAltOutlined onClick={onCopyUrlHandler} />
|
||||||
refetchAllView={refetchAllView}
|
{viewKey && (
|
||||||
/>
|
<Dropdown trigger={['click']} menu={moreOptionMenu}>
|
||||||
}
|
<MoreOutlined />
|
||||||
showArrow={false}
|
</Dropdown>
|
||||||
open={isOpen}
|
)}
|
||||||
onOpenChange={handleOpenChange}
|
</Space>
|
||||||
>
|
</OffSetCol>
|
||||||
<Button
|
</Row>
|
||||||
type={saveButtonType}
|
</ExplorerCardHeadContainer>
|
||||||
icon={saveButtonIcon}
|
)}
|
||||||
data-testid="traces-save-view-action"
|
|
||||||
>
|
<div>{children}</div>
|
||||||
{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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import styled, { CSSProperties } from 'styled-components';
|
|||||||
|
|
||||||
export const ExplorerCardHeadContainer = styled(Card)`
|
export const ExplorerCardHeadContainer = styled(Card)`
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
|
padding: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const OffSetCol = styled(Col)`
|
export const OffSetCol = styled(Col)`
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
|
|||||||
import { ActionItemProps } from 'container/LogDetailedView/ActionItem';
|
import { ActionItemProps } from 'container/LogDetailedView/ActionItem';
|
||||||
import { ILog } from 'types/api/logs/log';
|
import { ILog } from 'types/api/logs/log';
|
||||||
|
|
||||||
|
import { VIEWS } from './constants';
|
||||||
|
|
||||||
export type LogDetailProps = {
|
export type LogDetailProps = {
|
||||||
log: ILog | null;
|
log: ILog | null;
|
||||||
|
selectedTab: VIEWS;
|
||||||
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
|
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
|
||||||
Pick<ActionItemProps, 'onClickActionItem'> &
|
Partial<Pick<ActionItemProps, 'onClickActionItem'>> &
|
||||||
Pick<DrawerProps, 'onClose'>;
|
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';
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
import JSONView from 'container/LogDetailedView/JsonView';
|
import './LogDetails.styles.scss';
|
||||||
import TableView from 'container/LogDetailedView/TableView';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
|
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 { LogDetailProps } from './LogDetail.interfaces';
|
||||||
|
import QueryBuilderSearchWrapper from './QueryBuilderSearchWrapper';
|
||||||
|
|
||||||
function LogDetail({
|
function LogDetail({
|
||||||
log,
|
log,
|
||||||
onClose,
|
onClose,
|
||||||
onAddToQuery,
|
onAddToQuery,
|
||||||
onClickActionItem,
|
onClickActionItem,
|
||||||
|
selectedTab,
|
||||||
}: LogDetailProps): JSX.Element {
|
}: LogDetailProps): JSX.Element {
|
||||||
const items = useMemo(
|
const [, copyToClipboard] = useCopyToClipboard();
|
||||||
() => [
|
const [selectedView, setSelectedView] = useState<VIEWS>(selectedTab);
|
||||||
{
|
|
||||||
label: 'Table',
|
const [isFilterVisibile, setIsFilterVisible] = useState<boolean>(false);
|
||||||
key: '1',
|
|
||||||
children: log && (
|
const [contextQuery, setContextQuery] = useState<Query | undefined>();
|
||||||
<TableView
|
const [filters, setFilters] = useState<TagFilter | null>(null);
|
||||||
logData={log}
|
const [isEdit, setIsEdit] = useState<boolean>(false);
|
||||||
onAddToQuery={onAddToQuery}
|
|
||||||
onClickActionItem={onClickActionItem}
|
const isDarkMode = useIsDarkMode();
|
||||||
/>
|
|
||||||
),
|
const { notifications } = useNotifications();
|
||||||
},
|
|
||||||
{
|
const LogJsonData = log ? aggregateAttributesResourcesToString(log) : '';
|
||||||
label: 'JSON',
|
|
||||||
key: '2',
|
const handleModeChange = (e: RadioChangeEvent): void => {
|
||||||
children: log && <JSONView logData={log} />,
|
setSelectedView(e.target.value);
|
||||||
},
|
setIsEdit(false);
|
||||||
],
|
setIsFilterVisible(false);
|
||||||
[log, onAddToQuery, onClickActionItem],
|
};
|
||||||
);
|
|
||||||
|
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 (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
width="60%"
|
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"
|
placement="right"
|
||||||
closable
|
// closable
|
||||||
onClose={onClose}
|
onClose={drawerCloseHandler}
|
||||||
open={log !== null}
|
open={log !== null}
|
||||||
style={{ overscrollBehavior: 'contain' }}
|
style={{
|
||||||
|
overscrollBehavior: 'contain',
|
||||||
|
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
|
||||||
|
}}
|
||||||
|
className="log-detail-drawer"
|
||||||
destroyOnClose
|
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>
|
</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 './ListLogView.styles.scss';
|
||||||
import {
|
|
||||||
CopyFilled,
|
import { blue } from '@ant-design/colors';
|
||||||
ExpandAltOutlined,
|
|
||||||
LinkOutlined,
|
|
||||||
MonitorOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import Convert from 'ansi-to-html';
|
import Convert from 'ansi-to-html';
|
||||||
import { Button, Divider, Row, Typography } from 'antd';
|
import { Typography } from 'antd';
|
||||||
import LogsExplorerContext from 'container/LogsExplorerContext';
|
import LogDetail from 'components/LogDetail';
|
||||||
|
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import dompurify from 'dompurify';
|
import dompurify from 'dompurify';
|
||||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
|
||||||
// utils
|
// utils
|
||||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { useCopyToClipboard } from 'react-use';
|
|
||||||
// interfaces
|
// interfaces
|
||||||
import { IField } from 'types/api/logs/fields';
|
import { IField } from 'types/api/logs/fields';
|
||||||
import { ILog } from 'types/api/logs/log';
|
import { ILog } from 'types/api/logs/log';
|
||||||
|
|
||||||
// components
|
// components
|
||||||
import AddToQueryHOC, { AddToQueryHOCProps } from '../AddToQueryHOC';
|
import AddToQueryHOC, { AddToQueryHOCProps } from '../AddToQueryHOC';
|
||||||
import CopyClipboardHOC from '../CopyClipboardHOC';
|
import LogLinesActionButtons from '../LogLinesActionButtons/LogLinesActionButtons';
|
||||||
|
import LogStateIndicator, {
|
||||||
|
LogType,
|
||||||
|
} from '../LogStateIndicator/LogStateIndicator';
|
||||||
// styles
|
// styles
|
||||||
import {
|
import {
|
||||||
Container,
|
Container,
|
||||||
LogContainer,
|
LogContainer,
|
||||||
LogText,
|
LogText,
|
||||||
SelectedLog,
|
|
||||||
Text,
|
Text,
|
||||||
TextContainer,
|
TextContainer,
|
||||||
} from './styles';
|
} from './styles';
|
||||||
@@ -55,12 +52,10 @@ function LogGeneralField({ fieldKey, fieldValue }: LogFieldProps): JSX.Element {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TextContainer>
|
<TextContainer>
|
||||||
<Text ellipsis type="secondary">
|
<Text ellipsis type="secondary" className="log-field-key">
|
||||||
{`${fieldKey}: `}
|
{`${fieldKey} : `}
|
||||||
</Text>
|
</Text>
|
||||||
<CopyClipboardHOC textToCopy={fieldValue}>
|
<LogText dangerouslySetInnerHTML={html} className="log-value" />
|
||||||
<LogText dangerouslySetInnerHTML={html} />
|
|
||||||
</CopyClipboardHOC>
|
|
||||||
</TextContainer>
|
</TextContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -71,23 +66,23 @@ function LogSelectedField({
|
|||||||
onAddToQuery,
|
onAddToQuery,
|
||||||
}: LogSelectedFieldProps): JSX.Element {
|
}: LogSelectedFieldProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<SelectedLog>
|
<div className="log-selected-fields">
|
||||||
<AddToQueryHOC
|
<AddToQueryHOC
|
||||||
fieldKey={fieldKey}
|
fieldKey={fieldKey}
|
||||||
fieldValue={fieldValue}
|
fieldValue={fieldValue}
|
||||||
onAddToQuery={onAddToQuery}
|
onAddToQuery={onAddToQuery}
|
||||||
>
|
>
|
||||||
<Typography.Text>
|
<Typography.Text>
|
||||||
<span style={{ color: blue[4] }}>{fieldKey}</span>
|
<span style={{ color: blue[4] }} className="selected-log-field-key">
|
||||||
|
{fieldKey}
|
||||||
|
</span>
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</AddToQueryHOC>
|
</AddToQueryHOC>
|
||||||
<CopyClipboardHOC textToCopy={fieldValue}>
|
<Typography.Text ellipsis className="selected-log-kv">
|
||||||
<Typography.Text ellipsis>
|
<span className="selected-log-field-key">{': '}</span>
|
||||||
<span>{': '}</span>
|
<span className="selected-log-value">{fieldValue || "''"}</span>
|
||||||
<span style={{ color: orange[6] }}>{fieldValue || "''"}</span>
|
</Typography.Text>
|
||||||
</Typography.Text>
|
</div>
|
||||||
</CopyClipboardHOC>
|
|
||||||
</SelectedLog>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,31 +101,38 @@ function ListLogView({
|
|||||||
}: ListLogViewProps): JSX.Element {
|
}: ListLogViewProps): JSX.Element {
|
||||||
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
|
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
|
||||||
|
|
||||||
const [, setCopy] = useCopyToClipboard();
|
const [hasActionButtons, setHasActionButtons] = useState<boolean>(false);
|
||||||
const { notifications } = useNotifications();
|
|
||||||
const { isHighlighted, isLogsExplorerPage, onLogCopy } = useCopyLogLink(
|
const { isHighlighted, isLogsExplorerPage, onLogCopy } = useCopyLogLink(
|
||||||
logData.id,
|
logData.id,
|
||||||
);
|
);
|
||||||
const {
|
const {
|
||||||
activeLog: activeContextLog,
|
activeLog: activeContextLog,
|
||||||
|
onAddToQuery: handleAddToQuery,
|
||||||
onSetActiveLog: handleSetActiveContextLog,
|
onSetActiveLog: handleSetActiveContextLog,
|
||||||
onClearActiveLog: handleClearActiveContextLog,
|
onClearActiveLog: handleClearActiveContextLog,
|
||||||
} = useActiveLog();
|
} = useActiveLog();
|
||||||
|
|
||||||
|
const handlerClearActiveContextLog = useCallback(
|
||||||
|
(event: React.MouseEvent | React.KeyboardEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
handleClearActiveContextLog();
|
||||||
|
},
|
||||||
|
[handleClearActiveContextLog],
|
||||||
|
);
|
||||||
|
|
||||||
const handleDetailedView = useCallback(() => {
|
const handleDetailedView = useCallback(() => {
|
||||||
onSetActiveLog(logData);
|
onSetActiveLog(logData);
|
||||||
}, [logData, onSetActiveLog]);
|
}, [logData, onSetActiveLog]);
|
||||||
|
|
||||||
const handleShowContext = useCallback(() => {
|
const handleShowContext = useCallback(
|
||||||
handleSetActiveContextLog(logData);
|
(event: React.MouseEvent) => {
|
||||||
}, [logData, handleSetActiveContextLog]);
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
const handleCopyJSON = (): void => {
|
handleSetActiveContextLog(logData);
|
||||||
setCopy(JSON.stringify(logData, null, 2));
|
},
|
||||||
notifications.success({
|
[logData, handleSetActiveContextLog],
|
||||||
message: 'Copied to clipboard',
|
);
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatedSelecedFields = useMemo(
|
const updatedSelecedFields = useMemo(
|
||||||
() => selectedFields.filter((e) => e.name !== 'id'),
|
() => selectedFields.filter((e) => e.name !== 'id'),
|
||||||
@@ -145,83 +147,64 @@ function ListLogView({
|
|||||||
[flattenLogData.timestamp],
|
[flattenLogData.timestamp],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const logType = logData?.attributes_string?.log_level || LogType.INFO;
|
||||||
|
|
||||||
|
const handleMouseEnter = (): void => {
|
||||||
|
setHasActionButtons(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = (): void => {
|
||||||
|
setHasActionButtons(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container $isActiveLog={isHighlighted}>
|
<>
|
||||||
<div>
|
<Container
|
||||||
<LogContainer>
|
$isActiveLog={isHighlighted}
|
||||||
<>
|
onMouseEnter={handleMouseEnter}
|
||||||
<LogGeneralField fieldKey="log" fieldValue={flattenLogData.body} />
|
onMouseLeave={handleMouseLeave}
|
||||||
{flattenLogData.stream && (
|
onClick={handleDetailedView}
|
||||||
<LogGeneralField fieldKey="stream" fieldValue={flattenLogData.stream} />
|
>
|
||||||
)}
|
<div className="log-line">
|
||||||
<LogGeneralField fieldKey="timestamp" fieldValue={timestampValue} />
|
<LogStateIndicator type={logType} />
|
||||||
</>
|
<div>
|
||||||
</LogContainer>
|
<LogContainer>
|
||||||
<div>
|
<LogGeneralField fieldKey="Log" fieldValue={flattenLogData.body} />
|
||||||
{updatedSelecedFields.map((field) =>
|
{flattenLogData.stream && (
|
||||||
isValidLogField(flattenLogData[field.name] as never) ? (
|
<LogGeneralField fieldKey="Stream" fieldValue={flattenLogData.stream} />
|
||||||
<LogSelectedField
|
)}
|
||||||
key={field.name}
|
<LogGeneralField fieldKey="Timestamp" fieldValue={timestampValue} />
|
||||||
fieldKey={field.name}
|
|
||||||
fieldValue={flattenLogData[field.name] as never}
|
{updatedSelecedFields.map((field) =>
|
||||||
onAddToQuery={onAddToQuery}
|
isValidLogField(flattenLogData[field.name] as never) ? (
|
||||||
/>
|
<LogSelectedField
|
||||||
) : null,
|
key={field.name}
|
||||||
)}
|
fieldKey={field.name}
|
||||||
|
fieldValue={flattenLogData[field.name] as never}
|
||||||
|
onAddToQuery={onAddToQuery}
|
||||||
|
/>
|
||||||
|
) : null,
|
||||||
|
)}
|
||||||
|
</LogContainer>
|
||||||
|
</div>
|
||||||
</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 && (
|
{hasActionButtons && isLogsExplorerPage && (
|
||||||
<>
|
<LogLinesActionButtons
|
||||||
<Button
|
handleShowContext={handleShowContext}
|
||||||
size="small"
|
onLogCopy={onLogCopy}
|
||||||
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}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</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;
|
width: 100% !important;
|
||||||
margin-bottom: 0.3rem;
|
margin-bottom: 0.3rem;
|
||||||
|
cursor: pointer;
|
||||||
.ant-card-body {
|
.ant-card-body {
|
||||||
padding: 0.3rem 0.6rem;
|
padding: 0.3rem 0.6rem;
|
||||||
}
|
}
|
||||||
@@ -29,11 +30,13 @@ export const TextContainer = styled.div`
|
|||||||
|
|
||||||
export const LogContainer = styled.div`
|
export const LogContainer = styled.div`
|
||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const LogText = styled.div`
|
export const LogText = styled.div`
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
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 {
|
import './RawLogView.styles.scss';
|
||||||
ExpandAltOutlined,
|
|
||||||
LinkOutlined,
|
|
||||||
MonitorOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import Convert from 'ansi-to-html';
|
import Convert from 'ansi-to-html';
|
||||||
import { Button, DrawerProps, Tooltip } from 'antd';
|
import { DrawerProps } from 'antd';
|
||||||
import LogDetail from 'components/LogDetail';
|
import LogDetail from 'components/LogDetail';
|
||||||
|
import { VIEW_TYPES, VIEWS } from 'components/LogDetail/constants';
|
||||||
import LogsExplorerContext from 'container/LogsExplorerContext';
|
import LogsExplorerContext from 'container/LogsExplorerContext';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import dompurify from 'dompurify';
|
import dompurify from 'dompurify';
|
||||||
@@ -22,13 +20,12 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
|
import LogLinesActionButtons from '../LogLinesActionButtons/LogLinesActionButtons';
|
||||||
|
import LogStateIndicator, {
|
||||||
|
LogType,
|
||||||
|
} from '../LogStateIndicator/LogStateIndicator';
|
||||||
// styles
|
// styles
|
||||||
import {
|
import { RawLogContent, RawLogViewContainer } from './styles';
|
||||||
ActionButtonsWrapper,
|
|
||||||
ExpandIconWrapper,
|
|
||||||
RawLogContent,
|
|
||||||
RawLogViewContainer,
|
|
||||||
} from './styles';
|
|
||||||
import { RawLogViewProps } from './types';
|
import { RawLogViewProps } from './types';
|
||||||
|
|
||||||
const convert = new Convert();
|
const convert = new Convert();
|
||||||
@@ -45,7 +42,6 @@ function RawLogView({
|
|||||||
);
|
);
|
||||||
const {
|
const {
|
||||||
activeLog: activeContextLog,
|
activeLog: activeContextLog,
|
||||||
onSetActiveLog: handleSetActiveContextLog,
|
|
||||||
onClearActiveLog: handleClearActiveContextLog,
|
onClearActiveLog: handleClearActiveContextLog,
|
||||||
} = useActiveLog();
|
} = useActiveLog();
|
||||||
const {
|
const {
|
||||||
@@ -56,12 +52,15 @@ function RawLogView({
|
|||||||
} = useActiveLog();
|
} = useActiveLog();
|
||||||
|
|
||||||
const [hasActionButtons, setHasActionButtons] = useState<boolean>(false);
|
const [hasActionButtons, setHasActionButtons] = useState<boolean>(false);
|
||||||
|
const [selectedTab, setSelectedTab] = useState<VIEWS | undefined>();
|
||||||
|
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
const isReadOnlyLog = !isLogsExplorerPage || isReadOnly;
|
const isReadOnlyLog = !isLogsExplorerPage || isReadOnly;
|
||||||
|
|
||||||
const severityText = data.severity_text ? `${data.severity_text} |` : '';
|
const severityText = data.severity_text ? `${data.severity_text} |` : '';
|
||||||
|
|
||||||
|
const logType = data?.attributes_string?.log_level || LogType.INFO;
|
||||||
|
|
||||||
const text = useMemo(
|
const text = useMemo(
|
||||||
() =>
|
() =>
|
||||||
typeof data.timestamp === 'string'
|
typeof data.timestamp === 'string'
|
||||||
@@ -74,6 +73,7 @@ function RawLogView({
|
|||||||
if (activeContextLog || isReadOnly) return;
|
if (activeContextLog || isReadOnly) return;
|
||||||
|
|
||||||
onSetActiveLog(data);
|
onSetActiveLog(data);
|
||||||
|
setSelectedTab(VIEW_TYPES.OVERVIEW);
|
||||||
}, [activeContextLog, isReadOnly, data, onSetActiveLog]);
|
}, [activeContextLog, isReadOnly, data, onSetActiveLog]);
|
||||||
|
|
||||||
const handleCloseLogDetail: DrawerProps['onClose'] = useCallback(
|
const handleCloseLogDetail: DrawerProps['onClose'] = useCallback(
|
||||||
@@ -84,6 +84,7 @@ function RawLogView({
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
onClearActiveLog();
|
onClearActiveLog();
|
||||||
|
setSelectedTab(undefined);
|
||||||
},
|
},
|
||||||
[onClearActiveLog],
|
[onClearActiveLog],
|
||||||
);
|
);
|
||||||
@@ -104,9 +105,11 @@ function RawLogView({
|
|||||||
(event) => {
|
(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
handleSetActiveContextLog(data);
|
// handleSetActiveContextLog(data);
|
||||||
|
setSelectedTab(VIEW_TYPES.CONTEXT);
|
||||||
|
onSetActiveLog(data);
|
||||||
},
|
},
|
||||||
[data, handleSetActiveContextLog],
|
[data, onSetActiveLog],
|
||||||
);
|
);
|
||||||
|
|
||||||
const html = useMemo(
|
const html = useMemo(
|
||||||
@@ -123,37 +126,27 @@ function RawLogView({
|
|||||||
align="middle"
|
align="middle"
|
||||||
$isDarkMode={isDarkMode}
|
$isDarkMode={isDarkMode}
|
||||||
$isReadOnly={isReadOnly}
|
$isReadOnly={isReadOnly}
|
||||||
$isActiveLog={isHighlighted}
|
$isHightlightedLog={isHighlighted}
|
||||||
|
$isActiveLog={isActiveLog}
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
>
|
>
|
||||||
{!isReadOnly && (
|
<LogStateIndicator type={logType} />
|
||||||
<ExpandIconWrapper flex="30px">
|
|
||||||
<ExpandAltOutlined />
|
|
||||||
</ExpandIconWrapper>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<RawLogContent
|
<RawLogContent
|
||||||
$isReadOnly={isReadOnly}
|
$isReadOnly={isReadOnly}
|
||||||
$isActiveLog={isActiveLog}
|
$isActiveLog={isActiveLog}
|
||||||
|
$isDarkMode={isDarkMode}
|
||||||
$isTextOverflowEllipsisDisabled={isTextOverflowEllipsisDisabled}
|
$isTextOverflowEllipsisDisabled={isTextOverflowEllipsisDisabled}
|
||||||
linesPerRow={linesPerRow}
|
linesPerRow={linesPerRow}
|
||||||
dangerouslySetInnerHTML={html}
|
dangerouslySetInnerHTML={html}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{hasActionButtons && (
|
{hasActionButtons && (
|
||||||
<ActionButtonsWrapper>
|
<LogLinesActionButtons
|
||||||
<Tooltip title="Show Context">
|
handleShowContext={handleShowContext}
|
||||||
<Button
|
onLogCopy={onLogCopy}
|
||||||
size="small"
|
/>
|
||||||
icon={<MonitorOutlined />}
|
|
||||||
onClick={handleShowContext}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title="Copy Link">
|
|
||||||
<Button size="small" icon={<LinkOutlined />} onClick={onLogCopy} />
|
|
||||||
</Tooltip>
|
|
||||||
</ActionButtonsWrapper>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeContextLog && (
|
{activeContextLog && (
|
||||||
@@ -162,12 +155,15 @@ function RawLogView({
|
|||||||
onClose={handleClearActiveContextLog}
|
onClose={handleClearActiveContextLog}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<LogDetail
|
{selectedTab && (
|
||||||
log={activeLog}
|
<LogDetail
|
||||||
onClose={handleCloseLogDetail}
|
selectedTab={selectedTab}
|
||||||
onAddToQuery={onAddToQuery}
|
log={activeLog}
|
||||||
onClickActionItem={onAddToQuery}
|
onClose={handleCloseLogDetail}
|
||||||
/>
|
onAddToQuery={onAddToQuery}
|
||||||
|
onClickActionItem={onAddToQuery}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</RawLogViewContainer>
|
</RawLogViewContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { blue } from '@ant-design/colors';
|
import { blue } from '@ant-design/colors';
|
||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Col, Row, Space } from 'antd';
|
import { Col, Row, Space } from 'antd';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { getActiveLogBackground, getDefaultLogBackground } from 'utils/logs';
|
import { getActiveLogBackground, getDefaultLogBackground } from 'utils/logs';
|
||||||
@@ -9,20 +10,21 @@ export const RawLogViewContainer = styled(Row)<{
|
|||||||
$isDarkMode: boolean;
|
$isDarkMode: boolean;
|
||||||
$isReadOnly?: boolean;
|
$isReadOnly?: boolean;
|
||||||
$isActiveLog?: boolean;
|
$isActiveLog?: boolean;
|
||||||
|
$isHightlightedLog: boolean;
|
||||||
}>`
|
}>`
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-weight: 700;
|
|
||||||
font-size: 0.625rem;
|
display: flex;
|
||||||
line-height: 1.25rem;
|
alignitems: center;
|
||||||
|
|
||||||
transition: background-color 0.2s ease-in;
|
transition: background-color 0.2s ease-in;
|
||||||
|
|
||||||
${({ $isActiveLog }): string => getActiveLogBackground($isActiveLog)}
|
${({ $isActiveLog }): string => getActiveLogBackground($isActiveLog)}
|
||||||
|
|
||||||
${({ $isReadOnly, $isDarkMode, $isActiveLog }): string =>
|
${({ $isReadOnly, $isActiveLog, $isDarkMode }): string =>
|
||||||
$isActiveLog
|
$isActiveLog
|
||||||
? getActiveLogBackground()
|
? getActiveLogBackground($isActiveLog, $isDarkMode)
|
||||||
: getDefaultLogBackground($isReadOnly, $isDarkMode)}
|
: getDefaultLogBackground($isReadOnly, $isDarkMode)}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -30,13 +32,17 @@ export const ExpandIconWrapper = styled(Col)`
|
|||||||
color: ${blue[6]};
|
color: ${blue[6]};
|
||||||
padding: 0.25rem 0.375rem;
|
padding: 0.25rem 0.375rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 12px;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const RawLogContent = styled.div<RawLogContentProps>`
|
export const RawLogContent = styled.div<RawLogContentProps>`
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
font-family: Fira Code, monospace;
|
font-family: 'SF Mono', monospace;
|
||||||
font-weight: 300;
|
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, linesPerRow }): string =>
|
||||||
$isTextOverflowEllipsisDisabled
|
$isTextOverflowEllipsisDisabled
|
||||||
@@ -48,15 +54,12 @@ export const RawLogContent = styled.div<RawLogContentProps>`
|
|||||||
line-clamp: ${linesPerRow};
|
line-clamp: ${linesPerRow};
|
||||||
-webkit-box-orient: vertical;`};
|
-webkit-box-orient: vertical;`};
|
||||||
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
|
letter-spacing: -0.07px;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
|
|
||||||
cursor: ${({ $isActiveLog, $isReadOnly }): string =>
|
cursor: ${({ $isActiveLog, $isReadOnly }): string =>
|
||||||
$isActiveLog || $isReadOnly ? 'initial' : 'pointer'};
|
$isActiveLog || $isReadOnly ? 'initial' : 'pointer'};
|
||||||
|
|
||||||
${({ $isActiveLog, $isReadOnly }): string =>
|
|
||||||
$isReadOnly && $isActiveLog ? 'padding: 0 1.5rem;' : ''}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ActionButtonsWrapper = styled(Space)`
|
export const ActionButtonsWrapper = styled(Space)`
|
||||||
|
|||||||
@@ -12,5 +12,6 @@ export interface RawLogContentProps {
|
|||||||
linesPerRow: number;
|
linesPerRow: number;
|
||||||
$isReadOnly?: boolean;
|
$isReadOnly?: boolean;
|
||||||
$isActiveLog?: boolean;
|
$isActiveLog?: boolean;
|
||||||
|
$isDarkMode?: boolean;
|
||||||
$isTextOverflowEllipsisDisabled?: boolean;
|
$isTextOverflowEllipsisDisabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,21 @@
|
|||||||
import { TableProps } from 'antd';
|
import { TableProps } from 'antd';
|
||||||
import { CSSProperties } from 'react';
|
import { CSSProperties } from 'react';
|
||||||
|
|
||||||
export const defaultCellStyle: CSSProperties = {
|
export function getDefaultCellStyle(isDarkMode?: boolean): CSSProperties {
|
||||||
paddingTop: 4,
|
return {
|
||||||
paddingBottom: 6,
|
paddingTop: 4,
|
||||||
paddingRight: 8,
|
paddingBottom: 6,
|
||||||
paddingLeft: 8,
|
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 = {
|
export const defaultTableStyle: CSSProperties = {
|
||||||
minWidth: '40rem',
|
minWidth: '40rem',
|
||||||
|
|||||||
@@ -2,18 +2,22 @@ import styled from 'styled-components';
|
|||||||
|
|
||||||
interface TableBodyContentProps {
|
interface TableBodyContentProps {
|
||||||
linesPerRow: number;
|
linesPerRow: number;
|
||||||
|
isDarkMode?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TableBodyContent = styled.div<TableBodyContentProps>`
|
export const TableBodyContent = styled.div<TableBodyContentProps>`
|
||||||
margin-bottom: 0;
|
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;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: ${(props): number => props.linesPerRow};
|
-webkit-line-clamp: ${(props): number => props.linesPerRow};
|
||||||
line-clamp: ${(props): number => props.linesPerRow};
|
line-clamp: ${(props): number => props.linesPerRow};
|
||||||
-webkit-box-orient: vertical;
|
-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 {
|
import './useTableView.styles.scss';
|
||||||
ExpandAltOutlined,
|
|
||||||
LinkOutlined,
|
|
||||||
MonitorOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import Convert from 'ansi-to-html';
|
import Convert from 'ansi-to-html';
|
||||||
import { Button, Space, Typography } from 'antd';
|
import { Typography } from 'antd';
|
||||||
import { ColumnsType } from 'antd/es/table';
|
import { ColumnsType } from 'antd/es/table';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import dompurify from 'dompurify';
|
import dompurify from 'dompurify';
|
||||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { ExpandIconWrapper } from '../RawLogView/styles';
|
import LogStateIndicator from '../LogStateIndicator/LogStateIndicator';
|
||||||
import { defaultCellStyle, defaultTableStyle } from './config';
|
import { defaultTableStyle, getDefaultCellStyle } from './config';
|
||||||
import { TableBodyContent } from './styles';
|
import { TableBodyContent } from './styles';
|
||||||
import {
|
import {
|
||||||
ActionsColumnProps,
|
|
||||||
ColumnTypeRender,
|
ColumnTypeRender,
|
||||||
UseTableViewProps,
|
UseTableViewProps,
|
||||||
UseTableViewResult,
|
UseTableViewResult,
|
||||||
@@ -24,60 +20,15 @@ import {
|
|||||||
|
|
||||||
const convert = new Convert();
|
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 => {
|
export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||||
const {
|
const { logs, fields, linesPerRow, appendTo = 'center' } = props;
|
||||||
logs,
|
|
||||||
fields,
|
const isDarkMode = useIsDarkMode();
|
||||||
linesPerRow,
|
|
||||||
appendTo = 'center',
|
|
||||||
onOpenLogsContext,
|
|
||||||
onClickExpand,
|
|
||||||
} = props;
|
|
||||||
const { isLogsExplorerPage } = useCopyLogLink();
|
|
||||||
|
|
||||||
const flattenLogData = useMemo(() => logs.map((log) => FlatLogData(log)), [
|
const flattenLogData = useMemo(() => logs.map((log) => FlatLogData(log)), [
|
||||||
logs,
|
logs,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleClickExpand = useCallback(
|
|
||||||
(index: number): void => {
|
|
||||||
if (!onClickExpand) return;
|
|
||||||
|
|
||||||
onClickExpand(logs[index]);
|
|
||||||
},
|
|
||||||
[logs, onClickExpand],
|
|
||||||
);
|
|
||||||
|
|
||||||
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
|
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
|
||||||
const fieldColumns: ColumnsType<Record<string, unknown>> = fields
|
const fieldColumns: ColumnsType<Record<string, unknown>> = fields
|
||||||
.filter((e) => e.name !== 'id')
|
.filter((e) => e.name !== 'id')
|
||||||
@@ -87,7 +38,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
|||||||
key: name,
|
key: name,
|
||||||
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
|
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
|
||||||
props: {
|
props: {
|
||||||
style: defaultCellStyle,
|
style: getDefaultCellStyle(isDarkMode),
|
||||||
},
|
},
|
||||||
children: (
|
children: (
|
||||||
<Typography.Paragraph ellipsis={{ rows: linesPerRow }}>
|
<Typography.Paragraph ellipsis={{ rows: linesPerRow }}>
|
||||||
@@ -98,38 +49,25 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
return [
|
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',
|
title: 'timestamp',
|
||||||
dataIndex: 'timestamp',
|
dataIndex: 'timestamp',
|
||||||
key: 'timestamp',
|
key: 'timestamp',
|
||||||
// https://github.com/ant-design/ant-design/discussions/36886
|
// https://github.com/ant-design/ant-design/discussions/36886
|
||||||
render: (field): ColumnTypeRender<Record<string, unknown>> => {
|
render: (field, item): ColumnTypeRender<Record<string, unknown>> => {
|
||||||
const date =
|
const date =
|
||||||
typeof field === 'string'
|
typeof field === 'string'
|
||||||
? dayjs(field).format()
|
? dayjs(field).format()
|
||||||
: dayjs(field / 1e6).format();
|
: dayjs(field / 1e6).format();
|
||||||
return {
|
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)),
|
__html: convert.toHtml(dompurify.sanitize(field)),
|
||||||
}}
|
}}
|
||||||
linesPerRow={linesPerRow}
|
linesPerRow={linesPerRow}
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
...(appendTo === 'end' ? fieldColumns : []),
|
...(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>>)
|
|
||||||
: []),
|
|
||||||
];
|
];
|
||||||
}, [
|
}, [fields, appendTo, isDarkMode, linesPerRow]);
|
||||||
logs,
|
|
||||||
fields,
|
|
||||||
appendTo,
|
|
||||||
linesPerRow,
|
|
||||||
isLogsExplorerPage,
|
|
||||||
handleClickExpand,
|
|
||||||
onOpenLogsContext,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return { columns, dataSource: flattenLogData };
|
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 {
|
.app-layout {
|
||||||
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.app-content {
|
.app-content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: auto;
|
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 (
|
return (
|
||||||
<Layout className={isDarkMode ? 'darkMode' : 'lightMode'}>
|
<Layout className={isDarkMode ? 'darkMode' : 'lightMode'}>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
@@ -264,7 +279,11 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
<div className="app-content">
|
<div className="app-content">
|
||||||
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
|
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
|
||||||
<LayoutContent>
|
<LayoutContent>
|
||||||
<ChildrenContainer>
|
<ChildrenContainer
|
||||||
|
style={{
|
||||||
|
margin: isLogsView() ? 0 : ' 0 1rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{isToDisplayLayout && !renderFullScreen && <TopNav />}
|
{isToDisplayLayout && !renderFullScreen && <TopNav />}
|
||||||
{children}
|
{children}
|
||||||
</ChildrenContainer>
|
</ChildrenContainer>
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ export const LayoutContent = styled(LayoutComponent.Content)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const ChildrenContainer = styled.div`
|
export const ChildrenContainer = styled.div`
|
||||||
margin: 0 1rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -7,16 +7,18 @@ function EditRules({ initialValue, ruleId }: EditRulesProps): JSX.Element {
|
|||||||
const [formInstance] = Form.useForm();
|
const [formInstance] = Form.useForm();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormAlertRules
|
<div style={{ marginTop: '1rem' }}>
|
||||||
alertType={
|
<FormAlertRules
|
||||||
initialValue.alertType
|
alertType={
|
||||||
? (initialValue.alertType as AlertTypes)
|
initialValue.alertType
|
||||||
: AlertTypes.METRICS_BASED_ALERT
|
? (initialValue.alertType as AlertTypes)
|
||||||
}
|
: AlertTypes.METRICS_BASED_ALERT
|
||||||
formInstance={formInstance}
|
}
|
||||||
initialValue={initialValue}
|
formInstance={formInstance}
|
||||||
ruleId={ruleId}
|
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';
|
} from './styles';
|
||||||
import { filterOptions, getSelectOptions } from './utils';
|
import { filterOptions, getSelectOptions } from './utils';
|
||||||
|
|
||||||
function ExportPanel({ isLoading, onExport }: ExportPanelProps): JSX.Element {
|
function ExportPanelContainer({
|
||||||
|
isLoading,
|
||||||
|
onExport,
|
||||||
|
}: ExportPanelProps): JSX.Element {
|
||||||
const { t } = useTranslation(['dashboard']);
|
const { t } = useTranslation(['dashboard']);
|
||||||
|
|
||||||
const [selectedDashboardId, setSelectedDashboardId] = useState<string | null>(
|
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 { Dashboard } from 'types/api/dashboard/getAll';
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
import ExportPanelContainer from './ExportPanel';
|
import ExportPanelContainer from './ExportPanelContainer';
|
||||||
|
|
||||||
function ExportPanel({
|
function ExportPanel({
|
||||||
isLoading,
|
isLoading,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import GridPanelSwitch from 'container/GridPanelSwitch';
|
|||||||
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
|
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
|
||||||
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
||||||
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
||||||
|
import { Time as TimeV2 } from 'container/TopNav/DateTimeSelectionV2/config';
|
||||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { useResizeObserver } from 'hooks/useDimensions';
|
import { useResizeObserver } from 'hooks/useDimensions';
|
||||||
@@ -28,7 +29,7 @@ export interface ChartPreviewProps {
|
|||||||
query: Query | null;
|
query: Query | null;
|
||||||
graphType?: PANEL_TYPES;
|
graphType?: PANEL_TYPES;
|
||||||
selectedTime?: timePreferenceType;
|
selectedTime?: timePreferenceType;
|
||||||
selectedInterval?: Time;
|
selectedInterval?: Time | TimeV2;
|
||||||
headline?: JSX.Element;
|
headline?: JSX.Element;
|
||||||
alertDef?: AlertDef;
|
alertDef?: AlertDef;
|
||||||
userQueryKey?: string;
|
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 { Button, Tabs } from 'antd';
|
||||||
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { QueryBuilder } from 'container/QueryBuilder';
|
import { QueryBuilder } from 'container/QueryBuilder';
|
||||||
|
import { Atom, LucideAccessibility, Play, Terminal } from 'lucide-react';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
@@ -49,22 +52,51 @@ function QuerySection({
|
|||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
label: t('tab_qb'),
|
label: (
|
||||||
|
<Button className="nav-btns">
|
||||||
|
<Atom size={14} />
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
key: EQueryType.QUERY_BUILDER,
|
key: EQueryType.QUERY_BUILDER,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('tab_chquery'),
|
label: (
|
||||||
|
<Button className="nav-btns">
|
||||||
|
<Terminal size={14} />
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
key: EQueryType.CLICKHOUSE,
|
key: EQueryType.CLICKHOUSE,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const items = useMemo(
|
const items = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{ label: t('tab_qb'), key: EQueryType.QUERY_BUILDER },
|
{
|
||||||
{ label: t('tab_chquery'), key: EQueryType.CLICKHOUSE },
|
label: (
|
||||||
{ label: t('tab_promql'), key: EQueryType.PROM },
|
<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 => {
|
const renderTabs = (typ: AlertTypes): JSX.Element | null => {
|
||||||
@@ -73,40 +105,54 @@ function QuerySection({
|
|||||||
case AlertTypes.LOGS_BASED_ALERT:
|
case AlertTypes.LOGS_BASED_ALERT:
|
||||||
case AlertTypes.EXCEPTIONS_BASED_ALERT:
|
case AlertTypes.EXCEPTIONS_BASED_ALERT:
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<div className="alert-tabs">
|
||||||
type="card"
|
<Tabs
|
||||||
style={{ width: '100%' }}
|
type="card"
|
||||||
defaultActiveKey={EQueryType.QUERY_BUILDER}
|
style={{ width: '100%' }}
|
||||||
activeKey={queryCategory}
|
defaultActiveKey={EQueryType.QUERY_BUILDER}
|
||||||
onChange={handleQueryCategoryChange}
|
activeKey={queryCategory}
|
||||||
tabBarExtraContent={
|
onChange={handleQueryCategoryChange}
|
||||||
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
tabBarExtraContent={
|
||||||
<Button type="primary" onClick={runQuery}>
|
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
||||||
Run Query
|
<Button
|
||||||
</Button>
|
type="primary"
|
||||||
</span>
|
onClick={runQuery}
|
||||||
}
|
className="stage-run-query"
|
||||||
items={tabs}
|
icon={<Play size={14} />}
|
||||||
/>
|
>
|
||||||
|
Stage & Run Query
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
items={tabs}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
case AlertTypes.METRICS_BASED_ALERT:
|
case AlertTypes.METRICS_BASED_ALERT:
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<div className="alert-tabs">
|
||||||
type="card"
|
<Tabs
|
||||||
style={{ width: '100%' }}
|
type="card"
|
||||||
defaultActiveKey={EQueryType.QUERY_BUILDER}
|
style={{ width: '100%' }}
|
||||||
activeKey={queryCategory}
|
defaultActiveKey={EQueryType.QUERY_BUILDER}
|
||||||
onChange={handleQueryCategoryChange}
|
activeKey={queryCategory}
|
||||||
tabBarExtraContent={
|
onChange={handleQueryCategoryChange}
|
||||||
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
tabBarExtraContent={
|
||||||
<Button type="primary" onClick={runQuery}>
|
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
||||||
Run Query
|
<Button
|
||||||
</Button>
|
type="primary"
|
||||||
</span>
|
onClick={runQuery}
|
||||||
}
|
className="stage-run-query"
|
||||||
items={items}
|
icon={<Play size={14} />}
|
||||||
/>
|
>
|
||||||
|
Stage & Run Query
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
items={items}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -126,7 +172,7 @@ function QuerySection({
|
|||||||
<>
|
<>
|
||||||
<StepHeading> {t('alert_form_step1')}</StepHeading>
|
<StepHeading> {t('alert_form_step1')}</StepHeading>
|
||||||
<FormContainer>
|
<FormContainer>
|
||||||
<div style={{ display: 'flex' }}>{renderTabs(alertType)}</div>
|
<div>{renderTabs(alertType)}</div>
|
||||||
{renderQuerySection(queryCategory)}
|
{renderQuerySection(queryCategory)}
|
||||||
</FormContainer>
|
</FormContainer>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -76,6 +76,10 @@ export const FormContainer = styled(Card)`
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
|
.ant-card-body {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const TextareaMedium = styled(TextArea)`
|
export const TextareaMedium = styled(TextArea)`
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Card, Typography } from 'antd';
|
import { Card, Typography } from 'antd';
|
||||||
import LogDetail from 'components/LogDetail';
|
import LogDetail from 'components/LogDetail';
|
||||||
|
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||||
import ListLogView from 'components/Logs/ListLogView';
|
import ListLogView from 'components/Logs/ListLogView';
|
||||||
import RawLogView from 'components/Logs/RawLogView';
|
import RawLogView from 'components/Logs/RawLogView';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
@@ -13,7 +14,6 @@ import { Heading } from 'container/LogsTable/styles';
|
|||||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||||
import useFontFaceObserver from 'hooks/useFontObserver';
|
|
||||||
import { useEventSource } from 'providers/EventSource';
|
import { useEventSource } from 'providers/EventSource';
|
||||||
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -51,19 +51,6 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
|||||||
[logs, activeLogId],
|
[logs, activeLogId],
|
||||||
);
|
);
|
||||||
|
|
||||||
useFontFaceObserver(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
family: 'Fira Code',
|
|
||||||
weight: '300',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
options.format === 'raw',
|
|
||||||
{
|
|
||||||
timeout: 5000,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectedFields = convertKeysToColumnFields(options.selectColumns);
|
const selectedFields = convertKeysToColumnFields(options.selectColumns);
|
||||||
|
|
||||||
const getItemContent = useCallback(
|
const getItemContent = useCallback(
|
||||||
@@ -145,6 +132,7 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
|||||||
</InfinityWrapperStyled>
|
</InfinityWrapperStyled>
|
||||||
)}
|
)}
|
||||||
<LogDetail
|
<LogDetail
|
||||||
|
selectedTab={VIEW_TYPES.OVERVIEW}
|
||||||
log={activeLog}
|
log={activeLog}
|
||||||
onClose={onClearActiveLog}
|
onClose={onClearActiveLog}
|
||||||
onAddToQuery={onAddToQuery}
|
onAddToQuery={onAddToQuery}
|
||||||
|
|||||||
@@ -29,11 +29,11 @@ function ActionItem({
|
|||||||
() => (
|
() => (
|
||||||
<Col>
|
<Col>
|
||||||
<Button type="text" size="small" onClick={onClickHandler(OPERATORS.IN)}>
|
<Button type="text" size="small" onClick={onClickHandler(OPERATORS.IN)}>
|
||||||
<PlusCircleOutlined /> Filter for value
|
<PlusCircleOutlined size={12} /> Filter for value
|
||||||
</Button>
|
</Button>
|
||||||
<br />
|
<br />
|
||||||
<Button type="text" size="small" onClick={onClickHandler(OPERATORS.NIN)}>
|
<Button type="text" size="small" onClick={onClickHandler(OPERATORS.NIN)}>
|
||||||
<MinusCircleOutlined /> Filter out value
|
<MinusCircleOutlined size={12} /> Filter out value
|
||||||
</Button>
|
</Button>
|
||||||
</Col>
|
</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)`
|
export const TagContainer = styled(Tag)`
|
||||||
&&& {
|
&&& {
|
||||||
|
border-color: var(--bg-slate-400);
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
padding: 0.063rem 0.5rem;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.75rem;
|
font-size: var(--font-size-xs);
|
||||||
line-height: 1.25rem;
|
line-height: 1.25rem;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const TagLabel = styled.span`
|
export const TagLabel = styled.span`
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
font-size: 12px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const TagValue = styled.span`
|
export const TagValue = styled.span`
|
||||||
|
color: var(--text-sakura-400);
|
||||||
|
/* background-color: var(--bg-slate-400); */
|
||||||
text-transform: capitalize;
|
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 { TagContainer, TagLabel, TagValue } from './FieldRenderer.styles';
|
||||||
import { FieldRendererProps } from './LogDetailedView.types';
|
import { FieldRendererProps } from './LogDetailedView.types';
|
||||||
@@ -8,21 +10,29 @@ function FieldRenderer({ field }: FieldRendererProps): JSX.Element {
|
|||||||
const { dataType, newField, logType } = getFieldAttributes(field);
|
const { dataType, newField, logType } = getFieldAttributes(field);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span>
|
<span className="field-renderer-container">
|
||||||
{dataType && newField && logType ? (
|
{dataType && newField && logType ? (
|
||||||
<>
|
<>
|
||||||
<span style={{ color: blue[4] }}>{newField} </span>
|
<div className="label">{newField} </div>
|
||||||
<TagContainer>
|
|
||||||
<TagLabel>Type: </TagLabel>
|
<div className="tags">
|
||||||
<TagValue>{logType}</TagValue>
|
<TagContainer>
|
||||||
</TagContainer>
|
<TagLabel>
|
||||||
<TagContainer>
|
type
|
||||||
<TagLabel>Data type: </TagLabel>
|
<Divider type="vertical" />{' '}
|
||||||
<TagValue>{dataType}</TagValue>
|
</TagLabel>
|
||||||
</TagContainer>
|
<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>
|
</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 './JsonView.styles.scss';
|
||||||
import { CopyFilled } from '@ant-design/icons';
|
|
||||||
import { Button, Row } from 'antd';
|
import MEditor, { EditorProps, Monaco } from '@monaco-editor/react';
|
||||||
import Editor from 'components/Editor';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { useMemo } from 'react';
|
import { Switch, Typography } from 'antd';
|
||||||
import { useCopyToClipboard } from 'react-use';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { JSONViewProps } from './LogDetailedView.types';
|
import { JSONViewProps } from './LogDetailedView.types';
|
||||||
import { aggregateAttributesResourcesToString } from './utils';
|
import { aggregateAttributesResourcesToString } from './utils';
|
||||||
|
|
||||||
function JSONView({ logData }: JSONViewProps): JSX.Element {
|
function JSONView({ logData }: JSONViewProps): JSX.Element {
|
||||||
const [, copyToClipboard] = useCopyToClipboard();
|
const [isWrapWord, setIsWrapWord] = useState<boolean>(false);
|
||||||
|
|
||||||
const LogJsonData = useMemo(
|
const LogJsonData = useMemo(
|
||||||
() => aggregateAttributesResourcesToString(logData),
|
() => aggregateAttributesResourcesToString(logData),
|
||||||
[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 (
|
return (
|
||||||
<div>
|
<div className="json-view-container">
|
||||||
<Row
|
<MEditor
|
||||||
style={{
|
value={isWrapWord ? JSON.stringify(LogJsonData) : LogJsonData}
|
||||||
justifyContent: 'flex-end',
|
language="json"
|
||||||
margin: '0.5rem 0',
|
options={options}
|
||||||
}}
|
onChange={(): void => {}}
|
||||||
>
|
height="68vh"
|
||||||
<Button
|
theme={isDarkMode ? 'my-theme' : 'light'}
|
||||||
size="small"
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
type="text"
|
beforeMount={setEditorTheme}
|
||||||
onClick={(): void => copyToClipboard(LogJsonData)}
|
/>
|
||||||
>
|
|
||||||
<CopyFilled /> <span style={{ color: blue[5] }}>Copy to Clipboard</span>
|
<div className="json-view-footer">
|
||||||
</Button>
|
<div className="log-switch">
|
||||||
</Row>
|
<div className="wrap-word-switch">
|
||||||
<div style={{ marginTop: '0.5rem' }}>
|
<Typography.Text>Wrap text</Typography.Text>
|
||||||
<Editor
|
<Switch checked={isWrapWord} onChange={handleWrapWord} size="small" />
|
||||||
value={LogJsonData}
|
</div>
|
||||||
language="json"
|
</div>
|
||||||
height="70vh"
|
|
||||||
readOnly
|
|
||||||
onChange={(): void => {}}
|
|
||||||
/>
|
|
||||||
</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 { 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 { ColumnsType } from 'antd/es/table';
|
||||||
import AddToQueryHOC, {
|
import AddToQueryHOC, {
|
||||||
AddToQueryHOCProps,
|
AddToQueryHOCProps,
|
||||||
} from 'components/Logs/AddToQueryHOC';
|
} from 'components/Logs/AddToQueryHOC';
|
||||||
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
|
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
|
||||||
import { ResizeTable } from 'components/ResizeTable';
|
import { ResizeTable } from 'components/ResizeTable';
|
||||||
|
import { OPERATORS } from 'constants/queryBuilder';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { fieldSearchFilter } from 'lib/logs/fieldSearch';
|
import { fieldSearchFilter } from 'lib/logs/fieldSearch';
|
||||||
|
import { removeJSONStringifyQuotes } from 'lib/removeJSONStringifyQuotes';
|
||||||
import { isEmpty } from 'lodash-es';
|
import { isEmpty } from 'lodash-es';
|
||||||
|
import { ArrowDownToDot, ArrowUpFromDot } from 'lucide-react';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { generatePath } from 'react-router-dom';
|
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 { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
|
||||||
import { ILog } from 'types/api/logs/log';
|
import { ILog } from 'types/api/logs/log';
|
||||||
|
|
||||||
import ActionItem, { ActionItemProps } from './ActionItem';
|
import { ActionItemProps } from './ActionItem';
|
||||||
import FieldRenderer from './FieldRenderer';
|
import FieldRenderer from './FieldRenderer';
|
||||||
import {
|
import {
|
||||||
filterKeyForField,
|
filterKeyForField,
|
||||||
@@ -34,25 +39,53 @@ const RESTRICTED_FIELDS = ['timestamp'];
|
|||||||
|
|
||||||
interface TableViewProps {
|
interface TableViewProps {
|
||||||
logData: ILog;
|
logData: ILog;
|
||||||
|
fieldSearchInput: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = TableViewProps &
|
type Props = TableViewProps &
|
||||||
Pick<AddToQueryHOCProps, 'onAddToQuery'> &
|
Partial<Pick<ActionItemProps, 'onClickActionItem'>> &
|
||||||
Pick<ActionItemProps, 'onClickActionItem'>;
|
Pick<AddToQueryHOCProps, 'onAddToQuery'>;
|
||||||
|
|
||||||
function TableView({
|
function TableView({
|
||||||
logData,
|
logData,
|
||||||
|
fieldSearchInput,
|
||||||
onAddToQuery,
|
onAddToQuery,
|
||||||
onClickActionItem,
|
onClickActionItem,
|
||||||
}: Props): JSX.Element | null {
|
}: Props): JSX.Element | null {
|
||||||
const [fieldSearchInput, setFieldSearchInput] = useState<string>('');
|
|
||||||
|
|
||||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||||
|
const [isfilterInLoading, setIsFilterInLoading] = useState<boolean>(false);
|
||||||
|
const [isfilterOutLoading, setIsFilterOutLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
const flattenLogData: Record<string, string> | null = useMemo(
|
const flattenLogData: Record<string, string> | null = useMemo(
|
||||||
() => (logData ? flattenObject(logData) : null),
|
() => (logData ? flattenObject(logData) : null),
|
||||||
[logData],
|
[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) {
|
if (logData === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -95,24 +128,6 @@ function TableView({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const columns: ColumnsType<DataType> = [
|
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',
|
title: 'Field',
|
||||||
dataIndex: 'field',
|
dataIndex: 'field',
|
||||||
@@ -120,6 +135,7 @@ function TableView({
|
|||||||
width: 50,
|
width: 50,
|
||||||
align: 'left',
|
align: 'left',
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
|
className: 'attribute-name',
|
||||||
render: (field: string, record): JSX.Element => {
|
render: (field: string, record): JSX.Element => {
|
||||||
const renderedField = <FieldRenderer field={field} />;
|
const renderedField = <FieldRenderer field={field} />;
|
||||||
|
|
||||||
@@ -127,7 +143,7 @@ function TableView({
|
|||||||
const traceId = flattenLogData[record.field];
|
const traceId = flattenLogData[record.field];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space size="middle">
|
<Space size="middle" className="log-attribute">
|
||||||
{renderedField}
|
{renderedField}
|
||||||
|
|
||||||
{traceId && (
|
{traceId && (
|
||||||
@@ -166,15 +182,15 @@ function TableView({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Value',
|
title: 'Value',
|
||||||
dataIndex: 'value',
|
|
||||||
key: 'value',
|
key: 'value',
|
||||||
width: 70,
|
width: 70,
|
||||||
ellipsis: false,
|
ellipsis: false,
|
||||||
render: (field, record): JSX.Element => {
|
className: 'value-field-container attribute-value',
|
||||||
const textToCopy = field.slice(1, -1);
|
render: (fieldData: Record<string, string>, record): JSX.Element => {
|
||||||
|
const textToCopy = fieldData.value.slice(1, -1);
|
||||||
|
|
||||||
if (record.field === 'body') {
|
if (record.field === 'body') {
|
||||||
const parsedBody = recursiveParseJSON(field);
|
const parsedBody = recursiveParseJSON(fieldData.value);
|
||||||
if (!isEmpty(parsedBody)) {
|
if (!isEmpty(parsedBody)) {
|
||||||
return (
|
return (
|
||||||
<Tree defaultExpandAll showLine treeData={jsonToDataNodes(parsedBody)} />
|
<Tree defaultExpandAll showLine treeData={jsonToDataNodes(parsedBody)} />
|
||||||
@@ -182,30 +198,58 @@ function TableView({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fieldFilterKey = filterKeyForField(fieldData.field);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CopyClipboardHOC textToCopy={textToCopy}>
|
<div className="value-field">
|
||||||
<span style={{ color: orange[6] }}>{removeEscapeCharacters(field)}</span>
|
<CopyClipboardHOC textToCopy={textToCopy}>
|
||||||
</CopyClipboardHOC>
|
<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 (
|
return (
|
||||||
<>
|
<ResizeTable
|
||||||
<Input
|
columns={columns}
|
||||||
placeholder="Search field names"
|
tableLayout="fixed"
|
||||||
size="large"
|
dataSource={dataSource}
|
||||||
value={fieldSearchInput}
|
pagination={false}
|
||||||
onChange={(e): void => setFieldSearchInput(e.target.value)}
|
showHeader={false}
|
||||||
/>
|
className="attribute-table-container"
|
||||||
<ResizeTable
|
/>
|
||||||
columns={columns}
|
|
||||||
tableLayout="fixed"
|
|
||||||
dataSource={dataSource}
|
|
||||||
pagination={false}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import LogDetail from 'components/LogDetail';
|
import LogDetail from 'components/LogDetail';
|
||||||
|
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import { getGeneratedFilterQueryString } from 'lib/getGeneratedFilterQueryString';
|
import { getGeneratedFilterQueryString } from 'lib/getGeneratedFilterQueryString';
|
||||||
import getStep from 'lib/getStep';
|
import getStep from 'lib/getStep';
|
||||||
@@ -136,6 +137,7 @@ function LogDetailedView({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<LogDetail
|
<LogDetail
|
||||||
|
selectedTab={VIEW_TYPES.OVERVIEW}
|
||||||
log={detailedLog}
|
log={detailedLog}
|
||||||
onClose={onDrawerClose}
|
onClose={onDrawerClose}
|
||||||
onAddToQuery={handleAddToQuery}
|
onAddToQuery={handleAddToQuery}
|
||||||
|
|||||||
@@ -262,3 +262,7 @@ export const removeEscapeCharacters = (str: string): string =>
|
|||||||
};
|
};
|
||||||
return escapeMap[char as keyof typeof escapeMap];
|
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 {
|
import {
|
||||||
initialQueriesMap,
|
initialQueriesMap,
|
||||||
OPERATORS,
|
OPERATORS,
|
||||||
@@ -7,17 +8,26 @@ import {
|
|||||||
import ExplorerOrderBy from 'container/ExplorerOrderBy';
|
import ExplorerOrderBy from 'container/ExplorerOrderBy';
|
||||||
import { QueryBuilder } from 'container/QueryBuilder';
|
import { QueryBuilder } from 'container/QueryBuilder';
|
||||||
import { OrderByFilterProps } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces';
|
import { OrderByFilterProps } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces';
|
||||||
|
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
||||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||||
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||||
import { ButtonWrapperStyled } from 'pages/LogsExplorer/styles';
|
|
||||||
import { prepareQueryWithDefaultTimestamp } from 'pages/LogsExplorer/utils';
|
import { prepareQueryWithDefaultTimestamp } from 'pages/LogsExplorer/utils';
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
function LogExplorerQuerySection(): JSX.Element {
|
function LogExplorerQuerySection({
|
||||||
const { handleRunQuery, updateAllQueriesOperators } = useQueryBuilder();
|
selectedView,
|
||||||
|
}: {
|
||||||
|
selectedView: string;
|
||||||
|
}): JSX.Element {
|
||||||
|
const { currentQuery, updateAllQueriesOperators } = useQueryBuilder();
|
||||||
|
|
||||||
|
const query = currentQuery?.builder?.queryData[0] || null;
|
||||||
|
|
||||||
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||||
const defaultValue = useMemo(() => {
|
const defaultValue = useMemo(() => {
|
||||||
const updatedQuery = updateAllQueriesOperators(
|
const updatedQuery = updateAllQueriesOperators(
|
||||||
@@ -45,6 +55,12 @@ function LogExplorerQuerySection(): JSX.Element {
|
|||||||
return config;
|
return config;
|
||||||
}, [panelTypes]);
|
}, [panelTypes]);
|
||||||
|
|
||||||
|
const { handleChangeQueryData } = useQueryOperations({
|
||||||
|
index: 0,
|
||||||
|
query,
|
||||||
|
filterConfigs,
|
||||||
|
});
|
||||||
|
|
||||||
const renderOrderBy = useCallback(
|
const renderOrderBy = useCallback(
|
||||||
({ query, onChange }: OrderByFilterProps): JSX.Element => (
|
({ query, onChange }: OrderByFilterProps): JSX.Element => (
|
||||||
<ExplorerOrderBy query={query} onChange={onChange} />
|
<ExplorerOrderBy query={query} onChange={onChange} />
|
||||||
@@ -59,20 +75,34 @@ function LogExplorerQuerySection(): JSX.Element {
|
|||||||
[panelTypes, renderOrderBy],
|
[panelTypes, renderOrderBy],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleChangeTagFilters = useCallback(
|
||||||
|
(value: IBuilderQuery['filters']) => {
|
||||||
|
handleChangeQueryData('filters', value);
|
||||||
|
},
|
||||||
|
[handleChangeQueryData],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryBuilder
|
<>
|
||||||
panelType={panelTypes}
|
{selectedView === 'search' && (
|
||||||
config={{ initialDataSource: DataSource.LOGS, queryVariant: 'static' }}
|
<div className="qb-search-view-container">
|
||||||
filterConfigs={filterConfigs}
|
<QueryBuilderSearch
|
||||||
queryComponents={queryComponents}
|
query={query}
|
||||||
actions={
|
onChange={handleChangeTagFilters}
|
||||||
<ButtonWrapperStyled>
|
whereClauseConfig={filterConfigs?.filters}
|
||||||
<Button type="primary" onClick={handleRunQuery}>
|
/>
|
||||||
Run Query
|
</div>
|
||||||
</Button>
|
)}
|
||||||
</ButtonWrapperStyled>
|
|
||||||
}
|
{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 './ShowButton.styles.scss';
|
||||||
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
|
|
||||||
|
|
||||||
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 {
|
interface ShowButtonProps {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
@@ -16,20 +19,35 @@ function ShowButton({
|
|||||||
order,
|
order,
|
||||||
onClick,
|
onClick,
|
||||||
}: ShowButtonProps): JSX.Element {
|
}: 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 (
|
return (
|
||||||
<ShowButtonWrapper>
|
<Button
|
||||||
<Typography>
|
disabled={isLoading || isDisabled}
|
||||||
Showing 10 lines {order === ORDERBY_FILTERS.ASC ? 'after' : 'before'} match
|
loading={isLoading}
|
||||||
</Typography>
|
onClick={onClick}
|
||||||
<Button
|
icon={getIcons()}
|
||||||
size="small"
|
className={cx(
|
||||||
disabled={isLoading || isDisabled}
|
'show-more-button',
|
||||||
loading={isLoading}
|
order === ORDERBY_FILTERS.ASC ? 'up' : 'down',
|
||||||
onClick={onClick}
|
isDisabled && 'disabled',
|
||||||
>
|
)}
|
||||||
Show 10 more lines
|
>
|
||||||
</Button>
|
Load more
|
||||||
</ShowButtonWrapper>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { OrderByPayload } from 'types/api/queryBuilder/queryBuilderData';
|
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 LOGS_MORE_PAGE_SIZE = 10;
|
||||||
|
|
||||||
export const getOrderByTimestamp = (order: string): OrderByPayload => ({
|
export const getOrderByTimestamp = (order: string): OrderByPayload => ({
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import './LogsContextList.styles.scss';
|
||||||
|
|
||||||
import RawLogView from 'components/Logs/RawLogView';
|
import RawLogView from 'components/Logs/RawLogView';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
@@ -21,6 +23,7 @@ import { EmptyText, ListContainer } from './styles';
|
|||||||
import { getRequestData } from './utils';
|
import { getRequestData } from './utils';
|
||||||
|
|
||||||
interface LogsContextListProps {
|
interface LogsContextListProps {
|
||||||
|
className?: string;
|
||||||
isEdit: boolean;
|
isEdit: boolean;
|
||||||
query: Query;
|
query: Query;
|
||||||
log: ILog;
|
log: ILog;
|
||||||
@@ -29,6 +32,7 @@ interface LogsContextListProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function LogsContextList({
|
function LogsContextList({
|
||||||
|
className,
|
||||||
isEdit,
|
isEdit,
|
||||||
query,
|
query,
|
||||||
log,
|
log,
|
||||||
@@ -166,7 +170,7 @@ function LogsContextList({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={`context-logs-list ${className}`}>
|
||||||
{order === ORDERBY_FILTERS.ASC && (
|
{order === ORDERBY_FILTERS.ASC && (
|
||||||
<ShowButton
|
<ShowButton
|
||||||
isLoading={isFetching}
|
isLoading={isFetching}
|
||||||
@@ -183,6 +187,7 @@ function LogsContextList({
|
|||||||
{isFetching && <Spinner size="large" height="10rem" />}
|
{isFetching && <Spinner size="large" height="10rem" />}
|
||||||
|
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
|
className="virtuoso-list"
|
||||||
initialTopMostItemIndex={0}
|
initialTopMostItemIndex={0}
|
||||||
data={logs}
|
data={logs}
|
||||||
itemContent={getItemContent}
|
itemContent={getItemContent}
|
||||||
@@ -198,8 +203,12 @@ function LogsContextList({
|
|||||||
onClick={handleShowNextLines}
|
onClick={handleShowNextLines}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LogsContextList.defaultProps = {
|
||||||
|
className: '',
|
||||||
|
};
|
||||||
|
|
||||||
export default memo(LogsContextList);
|
export default memo(LogsContextList);
|
||||||
|
|||||||
@@ -1,19 +1,14 @@
|
|||||||
import { Space, Typography } from 'antd';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { themeColors } from 'constants/theme';
|
import { Typography } from 'antd';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
export const ListContainer = styled.div<{ $isDarkMode: boolean }>`
|
export const ListContainer = styled.div<{ $isDarkMode: boolean }>`
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 0 -1.5rem;
|
height: 21rem;
|
||||||
height: 10rem;
|
overflow: hidden;
|
||||||
overflow-y: scroll;
|
|
||||||
|
|
||||||
background-color: ${({ $isDarkMode }): string =>
|
background-color: ${({ $isDarkMode }): string =>
|
||||||
$isDarkMode ? themeColors.darkGrey : themeColors.lightgrey};
|
$isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100};
|
||||||
`;
|
|
||||||
|
|
||||||
export const ShowButtonWrapper = styled(Space)`
|
|
||||||
margin: 0.625rem 0;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const EmptyText = styled(Typography)`
|
export const EmptyText = styled(Typography)`
|
||||||
|
|||||||
@@ -2,10 +2,14 @@ import { Card } from 'antd';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
export const CardStyled = styled(Card)`
|
export const CardStyled = styled(Card)`
|
||||||
|
border: none !important;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 0.5rem 0 3.1rem 0;
|
margin-bottom: 16px;
|
||||||
.ant-card-body {
|
.ant-card-body {
|
||||||
height: 20vh;
|
height: 200px;
|
||||||
min-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';
|
import { ILog } from 'types/api/logs/log';
|
||||||
|
|
||||||
export interface LogsExplorerContextProps {
|
export interface LogsExplorerContextProps {
|
||||||
log: ILog;
|
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 = {
|
export const infinityDefaultStyles: CSSProperties = {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
overflowX: 'scroll',
|
overflowX: 'scroll',
|
||||||
|
marginTop: '15px',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,22 +1,13 @@
|
|||||||
import LogDetail from 'components/LogDetail';
|
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 { useTableView } from 'components/Logs/TableView/useTableView';
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
import LogsExplorerContext from 'container/LogsExplorerContext';
|
|
||||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import useDragColumns from 'hooks/useDragColumns';
|
import useDragColumns from 'hooks/useDragColumns';
|
||||||
import { getDraggedColumns } from 'hooks/useDragColumns/utils';
|
import { getDraggedColumns } from 'hooks/useDragColumns/utils';
|
||||||
import {
|
import { forwardRef, memo, useCallback, useMemo } from 'react';
|
||||||
cloneElement,
|
|
||||||
forwardRef,
|
|
||||||
memo,
|
|
||||||
ReactElement,
|
|
||||||
ReactNode,
|
|
||||||
useCallback,
|
|
||||||
useMemo,
|
|
||||||
} from 'react';
|
|
||||||
import {
|
import {
|
||||||
TableComponents,
|
TableComponents,
|
||||||
TableVirtuoso,
|
TableVirtuoso,
|
||||||
@@ -26,11 +17,8 @@ import { ILog } from 'types/api/logs/log';
|
|||||||
|
|
||||||
import { infinityDefaultStyles } from './config';
|
import { infinityDefaultStyles } from './config';
|
||||||
import { LogsCustomTable } from './LogsCustomTable';
|
import { LogsCustomTable } from './LogsCustomTable';
|
||||||
import {
|
import { TableHeaderCellStyled, TableRowStyled } from './styles';
|
||||||
TableCellStyled,
|
import TableRow from './TableRow';
|
||||||
TableHeaderCellStyled,
|
|
||||||
TableRowStyled,
|
|
||||||
} from './styles';
|
|
||||||
import { InfinityTableProps } from './types';
|
import { InfinityTableProps } from './types';
|
||||||
|
|
||||||
// eslint-disable-next-line react/function-component-definition
|
// eslint-disable-next-line react/function-component-definition
|
||||||
@@ -64,6 +52,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
|||||||
activeLog: activeContextLog,
|
activeLog: activeContextLog,
|
||||||
onSetActiveLog: handleSetActiveContextLog,
|
onSetActiveLog: handleSetActiveContextLog,
|
||||||
onClearActiveLog: handleClearActiveContextLog,
|
onClearActiveLog: handleClearActiveContextLog,
|
||||||
|
onAddToQuery: handleAddToQuery,
|
||||||
} = useActiveLog();
|
} = useActiveLog();
|
||||||
const {
|
const {
|
||||||
activeLog,
|
activeLog,
|
||||||
@@ -96,37 +85,16 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
|||||||
|
|
||||||
const itemContent = useCallback(
|
const itemContent = useCallback(
|
||||||
(index: number, log: Record<string, unknown>): JSX.Element => (
|
(index: number, log: Record<string, unknown>): JSX.Element => (
|
||||||
<>
|
<TableRow
|
||||||
{tableColumns.map((column) => {
|
tableColumns={tableColumns}
|
||||||
if (!column.render) return <td>Empty</td>;
|
index={index}
|
||||||
|
log={log}
|
||||||
const element: ColumnTypeRender<Record<string, unknown>> = column.render(
|
handleSetActiveContextLog={handleSetActiveContextLog}
|
||||||
log[column.key as keyof Record<string, unknown>],
|
logs={tableViewProps.logs}
|
||||||
log,
|
hasActions
|
||||||
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>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
),
|
),
|
||||||
[tableColumns, isDarkMode],
|
[handleSetActiveContextLog, tableColumns, tableViewProps.logs],
|
||||||
);
|
);
|
||||||
|
|
||||||
const tableHeader = useCallback(
|
const tableHeader = useCallback(
|
||||||
@@ -137,13 +105,14 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TableHeaderCellStyled
|
<TableHeaderCellStyled
|
||||||
|
$isTimestamp={column.key === 'timestamp'}
|
||||||
$isDarkMode={isDarkMode}
|
$isDarkMode={isDarkMode}
|
||||||
$isDragColumn={isDragColumn}
|
$isDragColumn={isDragColumn}
|
||||||
key={column.key}
|
key={column.key}
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
{...(isDragColumn && { className: 'dragHandler' })}
|
{...(isDragColumn && { className: 'dragHandler' })}
|
||||||
>
|
>
|
||||||
{column.title as string}
|
{(column.title as string).replace(/^\w/, (c) => c.toUpperCase())}
|
||||||
</TableHeaderCellStyled>
|
</TableHeaderCellStyled>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -152,6 +121,12 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
|||||||
[tableColumns, isDarkMode],
|
[tableColumns, isDarkMode],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleClickExpand = (index: number): void => {
|
||||||
|
if (!onSetActiveLog) return;
|
||||||
|
|
||||||
|
onSetActiveLog(tableViewProps.logs[index]);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TableVirtuoso
|
<TableVirtuoso
|
||||||
@@ -173,15 +148,21 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
|||||||
{...(infitiyTableProps?.onEndReached
|
{...(infitiyTableProps?.onEndReached
|
||||||
? { endReached: infitiyTableProps.onEndReached }
|
? { endReached: infitiyTableProps.onEndReached }
|
||||||
: {})}
|
: {})}
|
||||||
|
onClick={(event: any): void => {
|
||||||
|
handleClickExpand(event.target.parentElement.parentElement.dataset.index);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{activeContextLog && (
|
{activeContextLog && (
|
||||||
<LogsExplorerContext
|
<LogDetail
|
||||||
log={activeContextLog}
|
log={activeContextLog}
|
||||||
onClose={handleClearActiveContextLog}
|
onClose={handleClearActiveContextLog}
|
||||||
|
onAddToQuery={handleAddToQuery}
|
||||||
|
selectedTab={VIEW_TYPES.CONTEXT}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<LogDetail
|
<LogDetail
|
||||||
|
selectedTab={VIEW_TYPES.OVERVIEW}
|
||||||
log={activeLog}
|
log={activeLog}
|
||||||
onClose={onClearActiveLog}
|
onClose={onClearActiveLog}
|
||||||
onAddToQuery={onAddToQuery}
|
onAddToQuery={onAddToQuery}
|
||||||
|
|||||||
@@ -5,29 +5,23 @@ import { getActiveLogBackground } from 'utils/logs';
|
|||||||
interface TableHeaderCellStyledProps {
|
interface TableHeaderCellStyledProps {
|
||||||
$isDragColumn: boolean;
|
$isDragColumn: boolean;
|
||||||
$isDarkMode: boolean;
|
$isDarkMode: boolean;
|
||||||
|
$isTimestamp?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TableStyled = styled.table`
|
export const TableStyled = styled.table`
|
||||||
width: 100%;
|
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>`
|
export const TableCellStyled = styled.td<TableHeaderCellStyledProps>`
|
||||||
padding: 0.5rem;
|
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 =>
|
background-color: ${(props): string =>
|
||||||
props.$isDarkMode ? themeColors.black : themeColors.whiteCream};
|
props.$isDarkMode ? 'inherit' : themeColors.whiteCream};
|
||||||
|
|
||||||
color: ${(props): string =>
|
color: ${(props): string =>
|
||||||
props.$isDarkMode ? themeColors.white : themeColors.bckgGrey};
|
props.$isDarkMode ? themeColors.white : themeColors.bckgGrey};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// handle the light theme here
|
||||||
export const TableRowStyled = styled.tr<{
|
export const TableRowStyled = styled.tr<{
|
||||||
$isActiveLog: boolean;
|
$isActiveLog: boolean;
|
||||||
$isDarkMode: boolean;
|
$isDarkMode: boolean;
|
||||||
@@ -36,34 +30,39 @@ export const TableRowStyled = styled.tr<{
|
|||||||
${({ $isActiveLog }): string => getActiveLogBackground($isActiveLog)}
|
${({ $isActiveLog }): string => getActiveLogBackground($isActiveLog)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.log-line-action-buttons {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
${TableCellStyled} {
|
${TableCellStyled} {
|
||||||
${({ $isActiveLog, $isDarkMode }): string =>
|
${({ $isActiveLog, $isDarkMode }): string =>
|
||||||
$isActiveLog
|
$isActiveLog
|
||||||
? getActiveLogBackground()
|
? getActiveLogBackground()
|
||||||
: `background-color: ${
|
: `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>`
|
export const TableHeaderCellStyled = styled.th<TableHeaderCellStyledProps>`
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
border-inline-end: 1px solid rgba(253, 253, 253, 0.12);
|
font-size: 14px;
|
||||||
background-color: ${(props): string =>
|
font-style: normal;
|
||||||
!props.$isDarkMode ? themeColors.whiteCream : themeColors.bckgGrey};
|
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;' : '')}
|
${({ $isDragColumn }): string => ($isDragColumn ? 'cursor: col-resize;' : '')}
|
||||||
|
|
||||||
color: ${(props): string =>
|
color: ${(props): string =>
|
||||||
props.$isDarkMode ? themeColors.white : themeColors.bckgGrey};
|
props.$isDarkMode ? 'var(--bg-vanilla-100, #fff)' : themeColors.bckgGrey};
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
border-start-start-radius: 2px;
|
|
||||||
}
|
|
||||||
&:last-child {
|
|
||||||
border-start-end-radius: 2px;
|
|
||||||
border-inline-end: none;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -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 LogDetail from 'components/LogDetail';
|
||||||
|
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||||
// components
|
// components
|
||||||
import ListLogView from 'components/Logs/ListLogView';
|
import ListLogView from 'components/Logs/ListLogView';
|
||||||
import RawLogView from 'components/Logs/RawLogView';
|
import RawLogView from 'components/Logs/RawLogView';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { CARD_BODY_STYLE } from 'constants/card';
|
import { CARD_BODY_STYLE } from 'constants/card';
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
import ExplorerControlPanel from 'container/ExplorerControlPanel';
|
|
||||||
import { Heading } from 'container/LogsTable/styles';
|
|
||||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import useFontFaceObserver from 'hooks/useFontObserver';
|
|
||||||
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
||||||
// interfaces
|
// interfaces
|
||||||
import { ILog } from 'types/api/logs/log';
|
import { ILog } from 'types/api/logs/log';
|
||||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
import NoLogs from '../NoLogs/NoLogs';
|
||||||
import InfinityTableView from './InfinityTableView';
|
import InfinityTableView from './InfinityTableView';
|
||||||
import { LogsExplorerListProps } from './LogsExplorerList.interfaces';
|
import { LogsExplorerListProps } from './LogsExplorerList.interfaces';
|
||||||
import { InfinityWrapperStyled } from './styles';
|
import { InfinityWrapperStyled } from './styles';
|
||||||
@@ -46,7 +47,7 @@ function LogsExplorerList({
|
|||||||
onSetActiveLog,
|
onSetActiveLog,
|
||||||
} = useActiveLog();
|
} = useActiveLog();
|
||||||
|
|
||||||
const { options, config } = useOptionsMenu({
|
const { options } = useOptionsMenu({
|
||||||
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
||||||
dataSource: initialDataSource || DataSource.METRICS,
|
dataSource: initialDataSource || DataSource.METRICS,
|
||||||
aggregateOperator:
|
aggregateOperator:
|
||||||
@@ -58,19 +59,6 @@ function LogsExplorerList({
|
|||||||
[logs, activeLogId],
|
[logs, activeLogId],
|
||||||
);
|
);
|
||||||
|
|
||||||
useFontFaceObserver(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
family: 'Fira Code',
|
|
||||||
weight: '300',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
options.format === 'raw',
|
|
||||||
{
|
|
||||||
timeout: 5000,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectedFields = useMemo(
|
const selectedFields = useMemo(
|
||||||
() => convertKeysToColumnFields(options.selectColumns),
|
() => convertKeysToColumnFields(options.selectColumns),
|
||||||
[options],
|
[options],
|
||||||
@@ -137,7 +125,10 @@ function LogsExplorerList({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card style={{ width: '100%' }} bodyStyle={CARD_BODY_STYLE}>
|
<Card
|
||||||
|
style={{ width: '100%', marginTop: '20px' }}
|
||||||
|
bodyStyle={CARD_BODY_STYLE}
|
||||||
|
>
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
ref={ref}
|
ref={ref}
|
||||||
data={logs}
|
data={logs}
|
||||||
@@ -151,33 +142,23 @@ function LogsExplorerList({
|
|||||||
}, [isLoading, options, logs, onEndReached, getItemContent, selectedFields]);
|
}, [isLoading, options, logs, onEndReached, getItemContent, selectedFields]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="logs-list-view-container">
|
||||||
<ExplorerControlPanel
|
{!isLoading && logs.length === 0 && <NoLogs />}
|
||||||
selectedOptionFormat={options.format}
|
|
||||||
isLoading={isLoading}
|
|
||||||
isShowPageSize={false}
|
|
||||||
optionsMenuConfig={config}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{options.format !== 'table' && (
|
{!isLoading && logs.length > 0 && (
|
||||||
<Heading>
|
<>
|
||||||
<Typography.Text>Event</Typography.Text>
|
<InfinityWrapperStyled>{renderContent}</InfinityWrapperStyled>
|
||||||
</Heading>
|
|
||||||
|
<LogDetail
|
||||||
|
selectedTab={VIEW_TYPES.OVERVIEW}
|
||||||
|
log={activeLog}
|
||||||
|
onClose={onClearActiveLog}
|
||||||
|
onAddToQuery={onAddToQuery}
|
||||||
|
onClickActionItem={onAddToQuery}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
{!isLoading && logs.length === 0 && (
|
|
||||||
<Typography>No logs lines found</Typography>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<InfinityWrapperStyled>{renderContent}</InfinityWrapperStyled>
|
|
||||||
|
|
||||||
<LogDetail
|
|
||||||
log={activeLog}
|
|
||||||
onClose={onClearActiveLog}
|
|
||||||
onAddToQuery={onAddToQuery}
|
|
||||||
onClickActionItem={onAddToQuery}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 { initialQueriesMap } from 'constants/queryBuilder';
|
||||||
import { QueryTable } from 'container/QueryTable';
|
import { QueryTable } from 'container/QueryTable';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
@@ -16,6 +18,7 @@ function LogsExplorerTable({
|
|||||||
query={stagedQuery || initialQueriesMap.metrics}
|
query={stagedQuery || initialQueriesMap.metrics}
|
||||||
queryTableData={data}
|
queryTableData={data}
|
||||||
loading={isLoading}
|
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';
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
import TabLabel from 'components/TabLabel';
|
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 { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import {
|
import {
|
||||||
@@ -9,11 +14,12 @@ import {
|
|||||||
PANEL_TYPES,
|
PANEL_TYPES,
|
||||||
} from 'constants/queryBuilder';
|
} from 'constants/queryBuilder';
|
||||||
import { DEFAULT_PER_PAGE_VALUE } from 'container/Controls/config';
|
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 GoToTop from 'container/GoToTop';
|
||||||
import LogsExplorerChart from 'container/LogsExplorerChart';
|
import LogsExplorerChart from 'container/LogsExplorerChart';
|
||||||
import LogsExplorerList from 'container/LogsExplorerList';
|
import LogsExplorerList from 'container/LogsExplorerList';
|
||||||
import LogsExplorerTable from 'container/LogsExplorerTable';
|
import LogsExplorerTable from 'container/LogsExplorerTable';
|
||||||
|
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||||
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
|
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
|
||||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||||
import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils';
|
import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils';
|
||||||
@@ -22,10 +28,12 @@ import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
|||||||
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import useAxiosError from 'hooks/useAxiosError';
|
import useAxiosError from 'hooks/useAxiosError';
|
||||||
|
import useClickOutside from 'hooks/useClickOutside';
|
||||||
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||||
import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData';
|
import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData';
|
||||||
|
import { Sliders } from 'lucide-react';
|
||||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
@@ -38,18 +46,27 @@ import {
|
|||||||
Query,
|
Query,
|
||||||
TagFilter,
|
TagFilter,
|
||||||
} from 'types/api/queryBuilder/queryBuilderData';
|
} 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 { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
|
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
import { ActionsWrapper } from './LogsExplorerViews.styled';
|
function LogsExplorerViews({
|
||||||
|
selectedView,
|
||||||
function LogsExplorerViews(): JSX.Element {
|
showHistogram,
|
||||||
|
}: {
|
||||||
|
selectedView: string;
|
||||||
|
showHistogram: boolean;
|
||||||
|
}): JSX.Element {
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const { activeLogId, timeRange, onTimeRangeChange } = useCopyLogLink();
|
const { activeLogId, timeRange, onTimeRangeChange } = useCopyLogLink();
|
||||||
|
|
||||||
const { queryData: pageSize } = useUrlQueryData(
|
const { queryData: pageSize } = useUrlQueryData(
|
||||||
QueryParams.pageSize,
|
QueryParams.pageSize,
|
||||||
DEFAULT_PER_PAGE_VALUE,
|
DEFAULT_PER_PAGE_VALUE,
|
||||||
@@ -63,18 +80,24 @@ function LogsExplorerViews(): JSX.Element {
|
|||||||
|
|
||||||
// Context
|
// Context
|
||||||
const {
|
const {
|
||||||
|
initialDataSource,
|
||||||
currentQuery,
|
currentQuery,
|
||||||
stagedQuery,
|
stagedQuery,
|
||||||
panelType,
|
panelType,
|
||||||
updateAllQueriesOperators,
|
updateAllQueriesOperators,
|
||||||
} = useQueryBuilder();
|
} = useQueryBuilder();
|
||||||
|
|
||||||
|
const [selectedPanelType, setSelectedPanelType] = useState<PANEL_TYPES>(
|
||||||
|
panelType || PANEL_TYPES.LIST,
|
||||||
|
);
|
||||||
|
|
||||||
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
||||||
|
|
||||||
// State
|
// State
|
||||||
const [page, setPage] = useState<number>(1);
|
const [page, setPage] = useState<number>(1);
|
||||||
const [logs, setLogs] = useState<ILog[]>([]);
|
const [logs, setLogs] = useState<ILog[]>([]);
|
||||||
const [requestData, setRequestData] = useState<Query | null>(null);
|
const [requestData, setRequestData] = useState<Query | null>(null);
|
||||||
|
const [showFormatMenuItems, setShowFormatMenuItems] = useState(false);
|
||||||
|
|
||||||
const handleAxisError = useAxiosError();
|
const handleAxisError = useAxiosError();
|
||||||
|
|
||||||
@@ -147,6 +170,12 @@ function LogsExplorerViews(): JSX.Element {
|
|||||||
[currentQuery, updateAllQueriesOperators],
|
[currentQuery, updateAllQueriesOperators],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleModeChange = (e: RadioChangeEvent): void => {
|
||||||
|
setSelectedPanelType(e.target.value);
|
||||||
|
setShowFormatMenuItems(false);
|
||||||
|
handleExplorerTabChange(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: listChartData,
|
data: listChartData,
|
||||||
isFetching: isFetchingListChartData,
|
isFetching: isFetchingListChartData,
|
||||||
@@ -155,7 +184,7 @@ function LogsExplorerViews(): JSX.Element {
|
|||||||
enabled: !!listChartQuery && panelType === PANEL_TYPES.LIST,
|
enabled: !!listChartQuery && panelType === PANEL_TYPES.LIST,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data, isFetching, isError } = useGetExplorerQueryRange(
|
const { data, isLoading, isError } = useGetExplorerQueryRange(
|
||||||
requestData,
|
requestData,
|
||||||
panelType,
|
panelType,
|
||||||
{
|
{
|
||||||
@@ -327,12 +356,25 @@ function LogsExplorerViews(): JSX.Element {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
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);
|
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(() => {
|
useEffect(() => {
|
||||||
const currentParams = data?.params as Omit<LogTimeRange, 'pageSize'>;
|
const currentParams = data?.params as Omit<LogTimeRange, 'pageSize'>;
|
||||||
@@ -390,56 +432,11 @@ function LogsExplorerViews(): JSX.Element {
|
|||||||
panelType,
|
panelType,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const tabsItems: TabsProps['items'] = useMemo(
|
const { options, config } = useOptionsMenu({
|
||||||
() => [
|
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
||||||
{
|
dataSource: initialDataSource || DataSource.METRICS,
|
||||||
label: (
|
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
|
||||||
<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 chartData = useMemo(() => {
|
const chartData = useMemo(() => {
|
||||||
if (!stagedQuery) return [];
|
if (!stagedQuery) return [];
|
||||||
@@ -466,31 +463,122 @@ function LogsExplorerViews(): JSX.Element {
|
|||||||
return isGroupByExist ? data.payload.data.result : firstPayloadQueryArray;
|
return isGroupByExist ? data.payload.data.result : firstPayloadQueryArray;
|
||||||
}, [stagedQuery, panelType, data, listChartData, listQuery]);
|
}, [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 (
|
return (
|
||||||
<>
|
<div className="logs-explorer-views-container">
|
||||||
<LogsExplorerChart
|
{showHistogram && (
|
||||||
isLoading={isFetchingListChartData || isLoadingListChartData}
|
<LogsExplorerChart
|
||||||
data={chartData}
|
isLoading={isFetchingListChartData || isLoadingListChartData}
|
||||||
/>
|
data={chartData}
|
||||||
{stagedQuery && (
|
/>
|
||||||
<ActionsWrapper>
|
|
||||||
<ExportPanel
|
|
||||||
query={exportDefaultQuery}
|
|
||||||
isLoading={isUpdateDashboardLoading}
|
|
||||||
onExport={handleExport}
|
|
||||||
/>
|
|
||||||
</ActionsWrapper>
|
|
||||||
)}
|
)}
|
||||||
<Tabs
|
|
||||||
items={tabsItems}
|
<div className="logs-explorer-views-types">
|
||||||
defaultActiveKey={panelType || PANEL_TYPES.LIST}
|
<div className="views-tabs-container">
|
||||||
activeKey={panelType || PANEL_TYPES.LIST}
|
<Radio.Group
|
||||||
onChange={handleExplorerTabChange}
|
className="views-tabs"
|
||||||
destroyInactiveTabPane
|
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 />
|
<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 } from 'container/TopNav/DateTimeSelection/config';
|
||||||
|
import { Time as TimeV2 } from 'container/TopNav/DateTimeSelectionV2/config';
|
||||||
import { GetMinMaxPayload } from 'lib/getMinMax';
|
import { GetMinMaxPayload } from 'lib/getMinMax';
|
||||||
|
|
||||||
export const getGlobalTime = (
|
export const getGlobalTime = (
|
||||||
selectedTime: Time,
|
selectedTime: Time | TimeV2,
|
||||||
globalTime: GetMinMaxPayload,
|
globalTime: GetMinMaxPayload,
|
||||||
): GetMinMaxPayload | undefined => {
|
): GetMinMaxPayload | undefined => {
|
||||||
if (selectedTime === 'custom') {
|
if (selectedTime === 'custom') {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import './logsTable.styles.scss';
|
|||||||
|
|
||||||
import { Card, Typography } from 'antd';
|
import { Card, Typography } from 'antd';
|
||||||
import LogDetail from 'components/LogDetail';
|
import LogDetail from 'components/LogDetail';
|
||||||
|
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||||
// components
|
// components
|
||||||
import ListLogView from 'components/Logs/ListLogView';
|
import ListLogView from 'components/Logs/ListLogView';
|
||||||
import RawLogView from 'components/Logs/RawLogView';
|
import RawLogView from 'components/Logs/RawLogView';
|
||||||
@@ -9,7 +10,6 @@ import LogsTableView from 'components/Logs/TableView';
|
|||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { CARD_BODY_STYLE } from 'constants/card';
|
import { CARD_BODY_STYLE } from 'constants/card';
|
||||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||||
import useFontFaceObserver from 'hooks/useFontObserver';
|
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { Virtuoso } from 'react-virtuoso';
|
import { Virtuoso } from 'react-virtuoso';
|
||||||
@@ -37,19 +37,6 @@ function LogsTable(props: LogsTableProps): JSX.Element {
|
|||||||
onSetActiveLog,
|
onSetActiveLog,
|
||||||
} = useActiveLog();
|
} = useActiveLog();
|
||||||
|
|
||||||
useFontFaceObserver(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
family: 'Fira Code',
|
|
||||||
weight: '300',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
viewMode === 'raw',
|
|
||||||
{
|
|
||||||
timeout: 5000,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
logs,
|
logs,
|
||||||
fields: { selected },
|
fields: { selected },
|
||||||
@@ -125,6 +112,7 @@ function LogsTable(props: LogsTableProps): JSX.Element {
|
|||||||
|
|
||||||
{renderContent}
|
{renderContent}
|
||||||
<LogDetail
|
<LogDetail
|
||||||
|
selectedTab={VIEW_TYPES.OVERVIEW}
|
||||||
log={activeLog}
|
log={activeLog}
|
||||||
onClose={onClearActiveLog}
|
onClose={onClearActiveLog}
|
||||||
onAddToQuery={onAddToQuery}
|
onAddToQuery={onAddToQuery}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
.logs-card {
|
.logs-card {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,3 +3,9 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
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 { Button, Card, Flex, Input, Space, Typography } from 'antd';
|
||||||
import editUser from 'api/user/editUser';
|
import editUser from 'api/user/editUser';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { PencilIcon, UserSquare } from 'lucide-react';
|
import { PencilIcon } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
@@ -79,7 +79,6 @@ function UserInfo(): JSX.Element {
|
|||||||
<Card>
|
<Card>
|
||||||
<Space direction="vertical" size="middle">
|
<Space direction="vertical" size="middle">
|
||||||
<Flex gap={8}>
|
<Flex gap={8}>
|
||||||
<UserSquare />{' '}
|
|
||||||
<Typography.Title level={4} style={{ marginTop: 0 }}>
|
<Typography.Title level={4} style={{ marginTop: 0 }}>
|
||||||
User Details
|
User Details
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
|
|||||||
@@ -1,13 +1,44 @@
|
|||||||
import './MySettings.styles.scss';
|
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 '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 Password from './Password';
|
||||||
import UserInfo from './UserInfo';
|
import UserInfo from './UserInfo';
|
||||||
|
|
||||||
function MySettings(): JSX.Element {
|
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 (
|
return (
|
||||||
<Space
|
<Space
|
||||||
direction="vertical"
|
direction="vertical"
|
||||||
@@ -16,9 +47,32 @@ function MySettings(): JSX.Element {
|
|||||||
margin: '16px 0',
|
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">
|
<Button className="flexBtn" onClick={(): void => Logout()} type="primary">
|
||||||
<LogOut size={12} /> Logout
|
<LogOut size={12} /> Logout
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ function DashboardDescription(): JSX.Element {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card style={{ marginTop: '1rem' }}>
|
||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
<Col flex={1} span={9}>
|
<Col flex={1} span={9}>
|
||||||
<Typography.Title
|
<Typography.Title
|
||||||
|
|||||||
@@ -30,11 +30,10 @@ function QueryHeader({
|
|||||||
const [collapse, setCollapse] = useState(false);
|
const [collapse, setCollapse] = useState(false);
|
||||||
return (
|
return (
|
||||||
<QueryWrapper>
|
<QueryWrapper>
|
||||||
<Row style={{ justifyContent: 'space-between' }}>
|
<Row style={{ justifyContent: 'space-between', marginBottom: '1rem' }}>
|
||||||
<Row>
|
<Row>
|
||||||
<Button
|
<Button
|
||||||
type="default"
|
type="default"
|
||||||
ghost
|
|
||||||
icon={disabled ? <EyeInvisibleFilled /> : <EyeFilled />}
|
icon={disabled ? <EyeInvisibleFilled /> : <EyeFilled />}
|
||||||
onClick={onDisable}
|
onClick={onDisable}
|
||||||
>
|
>
|
||||||
@@ -42,7 +41,6 @@ function QueryHeader({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="default"
|
type="default"
|
||||||
ghost
|
|
||||||
icon={collapse ? <RightOutlined /> : <DownOutlined />}
|
icon={collapse ? <RightOutlined /> : <DownOutlined />}
|
||||||
onClick={(): void => setCollapse(!collapse)}
|
onClick={(): void => setCollapse(!collapse)}
|
||||||
/>
|
/>
|
||||||
@@ -51,7 +49,6 @@ function QueryHeader({
|
|||||||
{deletable && (
|
{deletable && (
|
||||||
<Button
|
<Button
|
||||||
type="default"
|
type="default"
|
||||||
ghost
|
|
||||||
danger
|
danger
|
||||||
icon={<DeleteOutlined />}
|
icon={<DeleteOutlined />}
|
||||||
onClick={onDelete}
|
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 { Button, Tabs, Typography } from 'antd';
|
||||||
import TextToolTip from 'components/TextToolTip';
|
import TextToolTip from 'components/TextToolTip';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
@@ -9,6 +11,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
|||||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||||
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
|
import { Atom, LucideAccessibility, Play, Terminal } from 'lucide-react';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
import {
|
import {
|
||||||
getNextWidgets,
|
getNextWidgets,
|
||||||
@@ -132,7 +135,11 @@ function QuerySection({
|
|||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
key: EQueryType.QUERY_BUILDER,
|
key: EQueryType.QUERY_BUILDER,
|
||||||
label: 'Query Builder',
|
label: (
|
||||||
|
<Button className="nav-btns">
|
||||||
|
<Atom size={14} />
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
tab: <Typography>Query Builder</Typography>,
|
tab: <Typography>Query Builder</Typography>,
|
||||||
children: (
|
children: (
|
||||||
<QueryBuilder panelType={selectedGraph} filterConfigs={filterConfigs} />
|
<QueryBuilder panelType={selectedGraph} filterConfigs={filterConfigs} />
|
||||||
@@ -140,39 +147,51 @@ function QuerySection({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: EQueryType.CLICKHOUSE,
|
key: EQueryType.CLICKHOUSE,
|
||||||
label: 'ClickHouse Query',
|
label: (
|
||||||
|
<Button className="nav-btns">
|
||||||
|
<Terminal size={14} />
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
tab: <Typography>ClickHouse Query</Typography>,
|
tab: <Typography>ClickHouse Query</Typography>,
|
||||||
children: <ClickHouseQueryContainer />,
|
children: <ClickHouseQueryContainer />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: EQueryType.PROM,
|
key: EQueryType.PROM,
|
||||||
label: 'PromQL',
|
label: (
|
||||||
|
<Button className="nav-btns">
|
||||||
|
<LucideAccessibility size={14} />
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
tab: <Typography>PromQL</Typography>,
|
tab: <Typography>PromQL</Typography>,
|
||||||
children: <PromQLQueryContainer />,
|
children: <PromQLQueryContainer />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<div className="dashboard-navigation">
|
||||||
type="card"
|
<Tabs
|
||||||
style={{ width: '100%' }}
|
type="card"
|
||||||
defaultActiveKey={currentQuery.queryType}
|
style={{ width: '100%' }}
|
||||||
activeKey={currentQuery.queryType}
|
defaultActiveKey={currentQuery.queryType}
|
||||||
onChange={handleQueryCategoryChange}
|
activeKey={currentQuery.queryType}
|
||||||
tabBarExtraContent={
|
onChange={handleQueryCategoryChange}
|
||||||
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
tabBarExtraContent={
|
||||||
<TextToolTip text="This will temporarily save the current query and graph state. This will persist across tab change" />
|
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
||||||
<Button
|
<TextToolTip text="This will temporarily save the current query and graph state. This will persist across tab change" />
|
||||||
loading={getWidgetQueryRange.isFetching}
|
<Button
|
||||||
type="primary"
|
loading={getWidgetQueryRange.isFetching}
|
||||||
onClick={handleRunQuery}
|
type="primary"
|
||||||
>
|
onClick={handleRunQuery}
|
||||||
Stage & Run Query
|
className="stage-run-query"
|
||||||
</Button>
|
icon={<Play size={14} />}
|
||||||
</span>
|
>
|
||||||
}
|
Stage & Run Query
|
||||||
items={items}
|
</Button>
|
||||||
/>
|
</span>
|
||||||
|
}
|
||||||
|
items={items}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,4 +6,8 @@ export const QueryContainer = styled(Card)`
|
|||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
min-height: 23.5%;
|
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 { ExpandAltOutlined } from '@ant-design/icons';
|
||||||
import LogDetail from 'components/LogDetail';
|
import LogDetail from 'components/LogDetail';
|
||||||
|
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||||
import { ILog } from 'types/api/logs/log';
|
import { ILog } from 'types/api/logs/log';
|
||||||
@@ -36,6 +37,7 @@ function LogsList({ logs }: LogsListProps): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<LogDetail
|
<LogDetail
|
||||||
|
selectedTab={VIEW_TYPES.OVERVIEW}
|
||||||
log={activeLog}
|
log={activeLog}
|
||||||
onClose={onClearActiveLog}
|
onClose={onClearActiveLog}
|
||||||
onAddToQuery={onAddToQuery}
|
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 './QueryBuilder.styles.scss';
|
||||||
import { Button, Col, Row } from 'antd';
|
|
||||||
|
import { Button, Col, Divider, Row } from 'antd';
|
||||||
import { MAX_FORMULAS, MAX_QUERIES } from 'constants/queryBuilder';
|
import { MAX_FORMULAS, MAX_QUERIES } from 'constants/queryBuilder';
|
||||||
// ** Hooks
|
// ** Hooks
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { DatabaseZap, Sigma } from 'lucide-react';
|
||||||
// ** Constants
|
// ** Constants
|
||||||
import { memo, useEffect, useMemo } from 'react';
|
import { memo, useEffect, useMemo, useRef } from 'react';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
// ** Components
|
// ** Components
|
||||||
@@ -15,7 +17,6 @@ import { QueryBuilderProps } from './QueryBuilder.interfaces';
|
|||||||
export const QueryBuilder = memo(function QueryBuilder({
|
export const QueryBuilder = memo(function QueryBuilder({
|
||||||
config,
|
config,
|
||||||
panelType: newPanelType,
|
panelType: newPanelType,
|
||||||
actions,
|
|
||||||
filterConfigs = {},
|
filterConfigs = {},
|
||||||
queryComponents,
|
queryComponents,
|
||||||
}: QueryBuilderProps): JSX.Element {
|
}: QueryBuilderProps): JSX.Element {
|
||||||
@@ -28,6 +29,8 @@ export const QueryBuilder = memo(function QueryBuilder({
|
|||||||
initialDataSource,
|
initialDataSource,
|
||||||
} = useQueryBuilder();
|
} = useQueryBuilder();
|
||||||
|
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
|
||||||
const currentDataSource = useMemo(
|
const currentDataSource = useMemo(
|
||||||
() =>
|
() =>
|
||||||
(config && config.queryVariant === 'static' && config.initialDataSource) ||
|
(config && config.queryVariant === 'static' && config.initialDataSource) ||
|
||||||
@@ -64,70 +67,126 @@ export const QueryBuilder = memo(function QueryBuilder({
|
|||||||
[currentQuery],
|
[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 (
|
return (
|
||||||
<Row style={{ width: '100%' }} gutter={[0, 20]} justify="start">
|
<Row
|
||||||
<Col span={24}>
|
style={{ width: '100%' }}
|
||||||
<Row gutter={[0, 50]}>
|
gutter={[0, 20]}
|
||||||
{currentQuery.builder.queryData.map((query, index) => (
|
justify="start"
|
||||||
<Col key={query.queryName} span={24}>
|
className="query-builder-container"
|
||||||
<Query
|
>
|
||||||
index={index}
|
<div className="new-query-formula-buttons-container">
|
||||||
isAvailableToDisable={isAvailableToDisableQuery}
|
<Button disabled={isDisabledQueryButton} onClick={addNewBuilderQuery}>
|
||||||
queryVariant={config?.queryVariant || 'dropdown'}
|
<DatabaseZap size={12} />
|
||||||
query={query}
|
</Button>
|
||||||
filterConfigs={filterConfigs}
|
|
||||||
queryComponents={queryComponents}
|
<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>
|
</Col>
|
||||||
))}
|
</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>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col span={24}>
|
<Col span={1} className="query-builder-mini-map">
|
||||||
<Row gutter={[20, 0]}>
|
{currentQuery.builder.queryData.map((query) => (
|
||||||
<Col>
|
<Button
|
||||||
<Button
|
disabled={isDisabledQueryButton}
|
||||||
disabled={isDisabledQueryButton}
|
className="query-btn"
|
||||||
type="primary"
|
key={query.queryName}
|
||||||
icon={<PlusOutlined />}
|
onClick={(): void => handleScrollIntoView('query', query.queryName)}
|
||||||
onClick={addNewBuilderQuery}
|
>
|
||||||
>
|
{query.queryName}
|
||||||
Query
|
</Button>
|
||||||
</Button>
|
))}
|
||||||
</Col>
|
|
||||||
<Col>
|
{currentQuery.builder.queryFormulas.map((formula) => (
|
||||||
<Button
|
<Button
|
||||||
disabled={isDisabledFormulaButton}
|
disabled={isDisabledFormulaButton}
|
||||||
onClick={addNewFormula}
|
className="formula-btn"
|
||||||
type="primary"
|
key={formula.queryName}
|
||||||
icon={<PlusOutlined />}
|
onClick={(): void => handleScrollIntoView('formula', formula.queryName)}
|
||||||
>
|
>
|
||||||
Formula
|
{formula.queryName}
|
||||||
</Button>
|
</Button>
|
||||||
</Col>
|
))}
|
||||||
{actions}
|
|
||||||
</Row>
|
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export const StyledInner = styled(Col)`
|
|||||||
width: fit-content;
|
width: fit-content;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
margin-bottom: 0.875rem;
|
margin-bottom: 0.875rem;
|
||||||
min-height: 1.375rem;
|
min-height: 1.375rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.filter-toggler {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
@@ -1,15 +1,11 @@
|
|||||||
import { Col, Row, Typography } from 'antd';
|
import { Col, Row, Typography } from 'antd';
|
||||||
|
import { MinusSquare, PlusSquare } from 'lucide-react';
|
||||||
import { Fragment, memo, ReactNode, useState } from 'react';
|
import { Fragment, memo, ReactNode, useState } from 'react';
|
||||||
|
|
||||||
// ** Types
|
// ** Types
|
||||||
import { AdditionalFiltersProps } from './AdditionalFiltersToggler.interfaces';
|
import { AdditionalFiltersProps } from './AdditionalFiltersToggler.interfaces';
|
||||||
// ** Styles
|
// ** Styles
|
||||||
import {
|
import { StyledInner, StyledLink } from './AdditionalFiltersToggler.styled';
|
||||||
StyledIconClose,
|
|
||||||
StyledIconOpen,
|
|
||||||
StyledInner,
|
|
||||||
StyledLink,
|
|
||||||
} from './AdditionalFiltersToggler.styled';
|
|
||||||
|
|
||||||
export const AdditionalFiltersToggler = memo(function AdditionalFiltersToggler({
|
export const AdditionalFiltersToggler = memo(function AdditionalFiltersToggler({
|
||||||
children,
|
children,
|
||||||
@@ -44,8 +40,13 @@ export const AdditionalFiltersToggler = memo(function AdditionalFiltersToggler({
|
|||||||
return (
|
return (
|
||||||
<Row>
|
<Row>
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<StyledInner onClick={handleToggleOpenFilters}>
|
<StyledInner onClick={handleToggleOpenFilters} style={{ marginBottom: 0 }}>
|
||||||
{isOpenedFilters ? <StyledIconClose /> : <StyledIconOpen />}
|
{isOpenedFilters ? (
|
||||||
|
<MinusSquare size={14} fill="#4E74F8" />
|
||||||
|
) : (
|
||||||
|
<PlusSquare size={14} fill="#4E74F8" />
|
||||||
|
)}
|
||||||
|
|
||||||
{!isOpenedFilters && (
|
{!isOpenedFilters && (
|
||||||
<Typography>Add conditions for {filtersTexts}</Typography>
|
<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