Compare commits

..

13 Commits

Author SHA1 Message Date
sawhil
b2b8988523 fix: minor ux fix 2025-03-29 02:56:12 +05:30
sawhil
a59944bb88 fix: minor failsafes added: 2025-03-29 02:52:34 +05:30
sawhil
b85f1f1df5 feat: added customondragselect in grid card full view to handle breaking flow 2025-03-29 02:36:48 +05:30
sawhil
cdbd3168b0 feat: added traces corelation in api monitoring charts 2025-03-29 02:25:46 +05:30
sawhil
fc54a1a7eb fix: console removed 2025-03-28 18:08:27 +05:30
sawhil
0c28746d0b feat: added third party api feature flag 2025-03-28 13:44:02 +05:30
sawhil
be44d6606a fix: domain list breaking for non available data 2025-03-28 03:11:23 +05:30
sawhil
bf7a31817f fix: title fixed 2025-03-28 02:51:15 +05:30
sawhil
492d846fb5 feat: analytics added 2025-03-28 02:45:49 +05:30
Sahil
b6a121c2bb feat: endpoint details feedback 2025-03-26 14:52:36 +05:30
Sahil
e09519a748 feat: added endpoint name and port in endpoint details 2025-03-26 01:13:29 +05:30
Sahil
cd3645eb16 fix: added new tag 2025-03-26 01:12:07 +05:30
Sahil
e7f00ab0cd feat: new dropdown styles 2025-03-26 01:11:42 +05:30
41 changed files with 718 additions and 1757 deletions

View File

@@ -65,5 +65,6 @@
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring",
"METRICS_EXPLORER": "SigNoz | Metrics Explorer",
"METRICS_EXPLORER_EXPLORER": "SigNoz | Metrics Explorer",
"METRICS_EXPLORER_VIEWS": "SigNoz | Metrics Explorer"
"METRICS_EXPLORER_VIEWS": "SigNoz | Metrics Explorer",
"API_MONITORING": "SigNoz | API Monitoring"
}

View File

@@ -207,7 +207,7 @@ export const PasswordReset = Loadable(
export const SomethingWentWrong = Loadable(
() =>
import(
/* webpackChunkName: "SomethingWentWrong" */ 'pages/SomethingWentWrong'
/* webpackChunkName: "ErrorBoundaryFallback" */ 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback'
),
);
@@ -299,10 +299,3 @@ export const MetricsExplorer = Loadable(
export const ApiMonitoring = Loadable(
() => import(/* webpackChunkName: "ApiMonitoring" */ 'pages/ApiMonitoring'),
);
export const DynamicVariableTest = Loadable(
() =>
import(
/* webpackChunkName: "DynamicVariableTest" */ 'pages/DynamicVariableTest'
),
);

View File

@@ -15,7 +15,6 @@ import {
CustomDomainSettings,
DashboardPage,
DashboardWidget,
DynamicVariableTest,
EditAlertChannelsAlerts,
EditRulesPage,
ErrorDetails,
@@ -506,13 +505,6 @@ const routes: AppRoutes[] = [
key: 'API_MONITORING',
isPrivate: true,
},
{
path: ROUTES.DYNAMIC_VARIABLE_TEST,
exact: true,
component: DynamicVariableTest,
key: 'DYNAMIC_VARIABLE_TEST',
isPrivate: true,
},
];
export const SUPPORT_ROUTE: AppRoutes = {

View File

@@ -1,231 +0,0 @@
.multi-select-container {
position: relative;
width: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, sans-serif;
}
.multi-select-label {
margin-bottom: 4px;
font-size: 14px;
color: rgba(0, 0, 0, 0.85);
}
.multi-select-input {
width: 100%;
min-height: 40px;
border: 1px solid #d9d9d9;
border-radius: 6px;
padding: 4px 8px;
display: flex;
align-items: center;
cursor: text;
background-color: #fff;
transition: all 0.3s;
&:hover {
border-color: #40a9ff;
}
&:focus,
&.multi-select-input-focused {
border-color: #40a9ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
outline: 0;
}
}
.multi-select-chips {
display: flex;
flex-wrap: wrap;
width: 100%;
gap: 4px;
align-items: center;
}
.multi-select-chip {
background-color: #f0f0f0;
border-radius: 4px;
padding: 2px 8px;
display: flex;
align-items: center;
gap: 4px;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 14px;
line-height: 22px;
.multi-select-chip-remove {
cursor: pointer;
font-size: 12px;
background: none;
border: none;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
color: rgba(0, 0, 0, 0.45);
&:hover {
color: #ff4d4f;
}
}
}
.multi-select-search {
flex: 1;
min-width: 50px;
border: none;
outline: none;
background: transparent;
&:focus {
border: none;
box-shadow: none;
}
// Override Ant Design's styles
.ant-input {
background: transparent;
&:focus {
box-shadow: none;
}
}
}
.multi-select-clear-all {
background: none;
border: none;
padding: 4px;
cursor: pointer;
color: rgba(0, 0, 0, 0.45);
display: flex;
align-items: center;
justify-content: center;
&:hover {
color: #ff4d4f;
}
}
.multi-select-dropdown {
position: absolute;
top: 100%;
left: 0;
width: 100%;
max-height: 400px;
border: 1px solid #d9d9d9;
border-radius: 6px;
margin-top: 4px;
background-color: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: 1050;
overflow: hidden;
display: flex;
flex-direction: column;
}
.multi-select-option {
padding: 8px 12px;
cursor: pointer;
&:hover {
background-color: #f5f5f5;
}
}
.multi-select-divider {
height: 1px;
background-color: #f0f0f0;
margin: 0;
}
.multi-select-section-label {
padding: 8px 12px;
font-weight: 500;
color: rgba(0, 0, 0, 0.45);
font-size: 12px;
background-color: #fafafa;
}
.multi-select-loading {
padding: 12px;
display: flex;
align-items: center;
gap: 12px;
justify-content: center;
}
.multi-select-no-results {
padding: 12px;
text-align: center;
color: rgba(0, 0, 0, 0.45);
}
.multi-select-options-container,
.multi-select-section-content {
overflow-y: auto;
/* For WebKit browsers */
&::-webkit-scrollbar {
width: 6px;
height: 6px;
}
&::-webkit-scrollbar-track {
background: #f0f0f0;
border-radius: 3px;
}
&::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 3px;
}
&::-webkit-scrollbar-thumb:hover {
background: #aaa;
}
/* For Firefox */
scrollbar-width: thin;
scrollbar-color: #ccc #f0f0f0;
}
.multi-select-error {
border-color: #ff4d4f;
&:hover,
&:focus,
&.multi-select-input-focused {
border-color: #ff7875;
box-shadow: 0 0 0 2px rgba(255, 77, 79, 0.2);
}
}
.multi-select-error-text {
color: #ff4d4f;
font-size: 14px;
line-height: 1.5;
margin-top: 4px;
}
.multi-select-disabled {
.multi-select-input {
background-color: #f5f5f5;
cursor: not-allowed;
color: rgba(0, 0, 0, 0.25);
border-color: #d9d9d9;
&:hover {
border-color: #d9d9d9;
}
}
.multi-select-chip {
color: rgba(0, 0, 0, 0.25);
background-color: #eee;
}
}

View File

@@ -1,595 +0,0 @@
/* eslint-disable sonarjs/cognitive-complexity */
import './MultiSelect.styles.scss';
import { CloseOutlined, SearchOutlined } from '@ant-design/icons';
import { Checkbox, Input, Spin } from 'antd';
import { CheckboxChangeEvent } from 'antd/es/checkbox';
import { InputRef } from 'antd/lib/input';
import { useCallback, useEffect, useRef, useState } from 'react';
export interface MultiSelectOption {
label: string;
value: string;
selected?: boolean;
disabled?: boolean;
}
export interface MultiSelectSection {
title: string;
options: MultiSelectOption[];
}
export interface MultiSelectProps {
/** Array of options to display in the dropdown */
options: MultiSelectOption[];
/** Callback when selected values change */
onChange: (selectedValues: string[]) => void;
/** Currently selected values */
value?: string[];
/** Placeholder text for the search input */
placeholder?: string;
/** Whether the component is in loading state */
loading?: boolean;
/** Allow users to add custom values */
allowCustomValues?: boolean;
/** Callback when search text changes - can be used for server filtering */
onSearch?: (searchText: string) => void;
/** Custom class name */
className?: string;
/** Additional sections to display (e.g., "Related Values") */
additionalSections?: MultiSelectSection[];
/** Show "Select All" option */
showSelectAll?: boolean;
/** Maximum height of dropdown in pixels */
dropdownMaxHeight?: number;
/** Maximum width of dropdown in pixels (defaults to matching input width) */
dropdownMaxWidth?: number;
/** Disable the component */
disabled?: boolean;
/** Error message to display */
error?: string;
/** Label text */
label?: string;
/** Allow users to clear all selections */
allowClear?: boolean;
/** Maximum height of a section */
sectionMaxHeight?: number;
}
function MultiSelect({
options,
onChange,
value = [],
placeholder = 'Search...',
loading = false,
allowCustomValues = true,
onSearch,
className = '',
additionalSections = [],
showSelectAll = true,
dropdownMaxHeight = 400,
dropdownMaxWidth,
disabled = false,
error,
label,
allowClear = true,
sectionMaxHeight = 150,
}: MultiSelectProps): JSX.Element {
const [isDropdownOpen, setIsDropdownOpen] = useState<boolean>(false);
const [searchText, setSearchText] = useState<string>('');
const [selectedValues, setSelectedValues] = useState<string[]>(value);
const [displayOptions, setDisplayOptions] = useState<MultiSelectOption[]>(
options,
);
const containerRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<InputRef>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
const [focusedChipIndex, setFocusedChipIndex] = useState<number>(-1);
const chipRefs = useRef<(HTMLDivElement | null)[]>([]);
// Handle save action - memoize with useCallback
const handleSave = useCallback((): void => {
setIsDropdownOpen(false);
setSearchText('');
onChange(selectedValues);
}, [onChange, selectedValues]);
// Synchronize value prop with internal state
useEffect(() => {
setSelectedValues(value);
}, [value]);
// Filter and sort options based on search text
useEffect(() => {
// Filter options based on search text
const filteredOptions = options.filter((option) =>
option.label.toLowerCase().includes(searchText.toLowerCase()),
);
// Add custom value option if no matches found and allowCustomValues is true
if (
allowCustomValues &&
searchText &&
!filteredOptions.some(
(option) => option.label.toLowerCase() === searchText.toLowerCase(),
) &&
!filteredOptions.some(
(option) => option.value.toLowerCase() === searchText.toLowerCase(),
)
) {
filteredOptions.unshift({
label: `Add "${searchText}"`,
value: searchText,
});
}
// Sort options: selected first, then matching search term
const sortedOptions = [...filteredOptions].sort((a, b) => {
// First by selection status
if (selectedValues.includes(a.value) && !selectedValues.includes(b.value))
return -1;
if (!selectedValues.includes(a.value) && selectedValues.includes(b.value))
return 1;
// Then by match position (exact matches or starts with come first)
const aLower = a.label.toLowerCase();
const bLower = b.label.toLowerCase();
const searchLower = searchText.toLowerCase();
if (aLower === searchLower && bLower !== searchLower) return -1;
if (aLower !== searchLower && bLower === searchLower) return 1;
if (aLower.startsWith(searchLower) && !bLower.startsWith(searchLower))
return -1;
if (!aLower.startsWith(searchLower) && bLower.startsWith(searchLower))
return 1;
return 0;
});
setDisplayOptions(sortedOptions);
}, [options, searchText, selectedValues, allowCustomValues]);
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent): void => {
if (
containerRef.current &&
!containerRef.current.contains(event.target as Node)
) {
handleSave();
}
};
document.addEventListener('mousedown', handleClickOutside);
return (): void => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [selectedValues, handleSave]);
// Adjust dropdown position if needed
useEffect(() => {
if (isDropdownOpen && dropdownRef.current && containerRef.current) {
const containerRect = containerRef.current.getBoundingClientRect();
const dropdownHeight = dropdownRef.current.offsetHeight;
const viewportHeight = window.innerHeight;
// Check if dropdown extends beyond viewport bottom
if (
containerRect.bottom + dropdownHeight > viewportHeight &&
containerRect.top > dropdownHeight
) {
dropdownRef.current.style.top = 'auto';
dropdownRef.current.style.bottom = '100%';
dropdownRef.current.style.marginTop = '0';
dropdownRef.current.style.marginBottom = '4px';
}
}
}, [isDropdownOpen, displayOptions]);
// Handle search input change
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
const text = e.target.value;
setSearchText(text);
if (onSearch) {
onSearch(text);
}
};
// Handle selection change
const handleSelectionChange = (
option: MultiSelectOption,
e: CheckboxChangeEvent,
): void => {
const { checked } = e.target;
let newSelectedValues: string[];
if (checked) {
newSelectedValues = [...selectedValues, option.value];
} else {
newSelectedValues = selectedValues.filter((val) => val !== option.value);
}
setSelectedValues(newSelectedValues);
};
// Handle "All" checkbox change
const handleSelectAll = (e: CheckboxChangeEvent): void => {
if (e.target.checked) {
const allValues = options
.filter((option) => !option.disabled)
.map((option) => option.value);
setSelectedValues(allValues);
} else {
setSelectedValues([]);
}
};
// Remove a selected item
const handleRemoveItem = useCallback(
(value: string): void => {
const newSelectedValues = selectedValues.filter((val) => val !== value);
setSelectedValues(newSelectedValues);
},
[selectedValues],
);
// Handle clicking the input area
const handleInputClick = (): void => {
if (!disabled) {
setIsDropdownOpen(true);
inputRef.current?.focus();
}
};
// Handle clear all selections
const handleClearAll = (): void => {
setSelectedValues([]);
setSearchText('');
inputRef.current?.focus();
};
// Get display value of a selection (chips)
const getSelectedOptions = (): MultiSelectOption[] =>
selectedValues.map((value) => {
const option = options.find((opt) => opt.value === value);
return {
label: option?.label || value,
value,
};
});
const selectedOptions = getSelectedOptions();
const allSelectableOptions = options.filter((option) => !option.disabled);
const allSelected =
allSelectableOptions.length > 0 &&
selectedValues.length === allSelectableOptions.length;
const containerClasses = [
'multi-select-container',
className,
disabled ? 'multi-select-disabled' : '',
error ? 'multi-select-error' : '',
]
.filter(Boolean)
.join(' ');
const inputClasses = [
'multi-select-input',
isDropdownOpen ? 'multi-select-input-focused' : '',
]
.filter(Boolean)
.join(' ');
// Reset chip refs array when selected options change
useEffect(() => {
chipRefs.current = Array(selectedOptions.length).fill(null);
}, [selectedOptions.length]);
// Handle chip keyboard navigation
const handleChipKeyDown = useCallback(
(e: React.KeyboardEvent, index: number) => {
e.stopPropagation(); // Prevent bubbling to container
switch (e.key) {
case 'ArrowLeft':
e.preventDefault();
// Move focus to previous chip
if (index > 0) {
setFocusedChipIndex(index - 1);
}
break;
case 'ArrowRight':
e.preventDefault();
// Move focus to next chip or input
if (index < selectedOptions.length - 1) {
setFocusedChipIndex(index + 1);
} else {
// Focus the input when at the last chip
setFocusedChipIndex(-1);
inputRef.current?.focus();
}
break;
case 'Delete':
case 'Backspace':
e.preventDefault();
// Remove current chip
handleRemoveItem(selectedOptions[index].value);
// Adjust focus after deletion
if (selectedOptions.length > 1) {
// Focus previous chip if not at beginning
const newIndex = Math.min(index, selectedOptions.length - 2);
setFocusedChipIndex(newIndex);
} else {
// If this was the last chip, focus input
setFocusedChipIndex(-1);
inputRef.current?.focus();
}
break;
case 'Escape':
e.preventDefault();
// Return focus to input
setFocusedChipIndex(-1);
inputRef.current?.focus();
break;
default:
// No-op for unhandled keys
break;
}
},
[selectedOptions, handleRemoveItem],
);
// Handle key events in the input
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {
if (e.key === 'Enter') {
// Add custom value on Enter if it doesn't exist
if (
allowCustomValues &&
searchText &&
!options.some(
(option) => option.value.toLowerCase() === searchText.toLowerCase(),
) &&
!options.some(
(option) => option.label.toLowerCase() === searchText.toLowerCase(),
)
) {
const newSelectedValues = [...selectedValues, searchText];
setSelectedValues(newSelectedValues);
setSearchText('');
} else if (isDropdownOpen) {
handleSave();
}
} else if (e.key === 'Escape') {
handleSave();
} else if (
e.key === 'Backspace' &&
!searchText &&
selectedValues.length > 0
) {
// Remove the last selected item when pressing backspace in an empty input
const newSelectedValues = [...selectedValues];
newSelectedValues.pop();
setSelectedValues(newSelectedValues);
} else if (e.key === 'Tab' && isDropdownOpen) {
// Close dropdown but keep focus within component
e.preventDefault();
handleSave();
}
// Add navigation TO chips when in input field
if (e.key === 'ArrowLeft' && !searchText && selectedOptions.length > 0) {
e.preventDefault();
setFocusedChipIndex(selectedOptions.length - 1);
}
};
// Focus the appropriate chip when focusedChipIndex changes
useEffect(() => {
if (focusedChipIndex >= 0 && chipRefs.current[focusedChipIndex]) {
chipRefs.current[focusedChipIndex]?.focus();
}
}, [focusedChipIndex]);
return (
<div className={containerClasses} ref={containerRef}>
{label && <div className="multi-select-label">{label}</div>}
<div
className={inputClasses}
onClick={handleInputClick}
onKeyDown={(e): void => {
if (e.key === 'Enter' || e.key === ' ') {
handleInputClick();
}
}}
role="combobox"
aria-expanded={isDropdownOpen}
aria-haspopup="listbox"
aria-controls="multi-select-dropdown"
aria-owns="multi-select-dropdown"
tabIndex={disabled ? -1 : 0}
>
<div className="multi-select-chips">
{selectedOptions.map((option, index) => (
<div
key={option.value}
className={`multi-select-chip ${
focusedChipIndex === index ? 'multi-select-chip-focused' : ''
}`}
ref={(el): void => {
chipRefs.current[index] = el;
}}
role="button"
tabIndex={0}
onKeyDown={(e): void => handleChipKeyDown(e, index)}
onFocus={(): void => setFocusedChipIndex(index)}
onClick={(e): void => e.stopPropagation()}
aria-label={`Selected option: ${option.label}`}
>
{option.label}
{!disabled && (
<button
type="button"
className="multi-select-chip-remove"
onClick={(e): void => {
e.stopPropagation();
handleRemoveItem(option.value);
}}
aria-label={`Remove ${option.label}`}
tabIndex={-1} // Don't make the inner button tabbable
>
<CloseOutlined />
</button>
)}
</div>
))}
<Input
ref={inputRef}
className="multi-select-search"
placeholder={selectedOptions.length === 0 ? placeholder : ''}
value={searchText}
onChange={handleSearchChange}
onKeyDown={handleKeyDown}
onFocus={(): void => setIsDropdownOpen(true)}
suffix={<SearchOutlined />}
bordered={false}
disabled={disabled}
/>
{allowClear && selectedValues.length > 0 && !disabled && (
<button
type="button"
className="multi-select-clear-all"
onClick={(e): void => {
e.stopPropagation();
handleClearAll();
}}
aria-label="Clear all selections"
>
<CloseOutlined />
</button>
)}
</div>
</div>
{error && <div className="multi-select-error-text">{error}</div>}
{isDropdownOpen && !disabled && (
<div
className="multi-select-dropdown"
ref={dropdownRef}
style={{
maxHeight: `${dropdownMaxHeight}px`,
maxWidth: dropdownMaxWidth ? `${dropdownMaxWidth}px` : undefined,
}}
id="multi-select-dropdown"
role="listbox"
aria-multiselectable="true"
>
{loading ? (
<div className="multi-select-loading">
<Spin size="small" />
<span>We are updating the values ...</span>
</div>
) : (
<>
{showSelectAll && (
<>
<div className="multi-select-option">
<Checkbox checked={allSelected} onChange={handleSelectAll}>
ALL
</Checkbox>
</div>
<div className="multi-select-divider" />
</>
)}
<div
className="multi-select-options-container"
style={{ maxHeight: `${sectionMaxHeight}px` }}
>
{displayOptions.length > 0 ? (
displayOptions.map((option) => (
<div
key={option.value}
className="multi-select-option"
role="option"
aria-selected={selectedValues.includes(option.value)}
>
<Checkbox
checked={selectedValues.includes(option.value)}
onChange={(e): void => handleSelectionChange(option, e)}
disabled={option.disabled}
>
{option.label}
</Checkbox>
</div>
))
) : (
<div className="multi-select-no-results">
{allowCustomValues && searchText
? `Add "${searchText}"`
: 'No results found'}
</div>
)}
</div>
{additionalSections.map(
(section) =>
section.options.length > 0 && (
<div key={`section-${section.title}`}>
<div className="multi-select-divider" />
<div className="multi-select-section-label">{section.title}</div>
<div
className="multi-select-section-content"
style={{ maxHeight: `${sectionMaxHeight}px` }}
>
{section.options.map((option) => (
<div
key={option.value}
className="multi-select-option"
role="option"
aria-selected={selectedValues.includes(option.value)}
>
<Checkbox
checked={selectedValues.includes(option.value)}
onChange={(e): void => handleSelectionChange(option, e)}
disabled={option.disabled}
>
{option.label}
</Checkbox>
</div>
))}
</div>
</div>
),
)}
</>
)}
</div>
)}
</div>
);
}
// Define defaultProps to fix linter warnings
MultiSelect.defaultProps = {
value: [],
placeholder: 'Search...',
loading: false,
allowCustomValues: true,
onSearch: undefined,
className: '',
additionalSections: [],
showSelectAll: true,
dropdownMaxHeight: 400,
dropdownMaxWidth: undefined,
disabled: false,
error: undefined,
label: undefined,
allowClear: true,
sectionMaxHeight: 150,
};
export default MultiSelect;

View File

@@ -1,8 +0,0 @@
import MultiSelect from './MultiSelect';
export type {
MultiSelectOption,
MultiSelectProps,
MultiSelectSection,
} from './MultiSelect';
export default MultiSelect;

View File

@@ -25,4 +25,5 @@ export enum FeatureKeys {
ANOMALY_DETECTION = 'ANOMALY_DETECTION',
AWS_INTEGRATION = 'AWS_INTEGRATION',
ONBOARDING_V3 = 'ONBOARDING_V3',
THIRD_PARTY_API = 'THIRD_PARTY_API',
}

View File

@@ -75,7 +75,6 @@ const ROUTES = {
METRICS_EXPLORER_BASE: '/metrics-explorer',
WORKSPACE_ACCESS_RESTRICTED: '/workspace-access-restricted',
HOME_PAGE: '/',
DYNAMIC_VARIABLE_TEST: '/dynamic-variable-test',
} as const;
export default ROUTES;

View File

@@ -1,5 +1,6 @@
import { LoadingOutlined } from '@ant-design/icons';
import { Select, Spin, Table, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import {
@@ -151,6 +152,7 @@ function AllEndPoints({
if (groupBy.length === 0) {
setSelectedEndPointName(record.endpointName); // this will open up the endpoint details tab
setSelectedView(VIEW_TYPES.ENDPOINT_DETAILS);
logEvent('API Monitoring: Endpoint name row clicked', {});
} else {
handleGroupByRowClick(record); // this will prepare the nested query payload
}

View File

@@ -392,6 +392,39 @@
gap: 20px;
padding-top: 20px;
.endpoint-meta-data {
display: flex;
gap: 8px;
.endpoint-meta-data-pill {
display: flex;
align-items: flex-start;
border-radius: 4px;
border: 1px solid var(--bg-slate-300);
width: fit-content;
.endpoint-meta-data-label {
display: flex;
padding: 6px 8px;
align-items: center;
gap: 4px;
border-right: 1px solid var(--bg-slate-300);
color: var(--text-vanilla-100);
background: var(--bg-slate-500);
height: calc(100% - 12px);
}
.endpoint-meta-data-value {
display: flex;
padding: 6px 8px;
justify-content: center;
align-items: center;
gap: 10px;
color: var(--text-vanilla-400);
background: var(--bg-slate-400);
height: calc(100% - 12px);
}
}
}
.endpoint-details-filters-container {
display: flex;
flex-direction: row;
@@ -405,6 +438,13 @@
}
}
.ant-select-item,
.ant-select-item-option-content {
flex: auto;
white-space: normal;
overflow-wrap: break-word;
}
.status-code-table-container {
border-radius: 3px;
border: 1px solid var(--bg-slate-500);
@@ -809,6 +849,13 @@
width: 100%;
}
}
.ant-select-item,
.ant-select-item-option-content {
flex: auto;
white-space: normal;
overflow-wrap: break-word;
}
}
.lightMode {
@@ -917,6 +964,20 @@
}
}
.endpoint-meta-data {
.endpoint-meta-data-pill {
.endpoint-meta-data-label {
color: var(--text-ink-300);
background: var(--bg-vanilla-100);
}
.endpoint-meta-data-value {
color: var(--text-ink-300);
background: var(--bg-vanilla-100);
}
}
}
.status-code-table-container {
.ant-table {
.ant-table-thead > tr > th {

View File

@@ -19,12 +19,14 @@ function DomainDetails({
selectedDomainIndex,
setSelectedDomainIndex,
domainListLength,
domainListFilters,
}: {
domainData: any;
handleClose: () => void;
selectedDomainIndex: number;
setSelectedDomainIndex: (index: number) => void;
domainListLength: number;
domainListFilters: IBuilderQuery['filters'];
}): JSX.Element {
const [selectedView, setSelectedView] = useState<VIEWS>(VIEWS.ALL_ENDPOINTS);
const [selectedEndPointName, setSelectedEndPointName] = useState<string>('');
@@ -132,6 +134,7 @@ function DomainDetails({
domainName={domainData.domainName}
endPointName={selectedEndPointName}
setSelectedEndPointName={setSelectedEndPointName}
domainListFilters={domainListFilters}
/>
)}
</>

View File

@@ -2,7 +2,10 @@ import { ENTITY_VERSION_V4 } from 'constants/app';
import { initialQueriesMap } from 'constants/queryBuilder';
import {
END_POINT_DETAILS_QUERY_KEYS_ARRAY,
extractPortAndEndpoint,
getEndPointDetailsQueryPayload,
getLatencyOverTimeWidgetData,
getRateOverTimeWidgetData,
} from 'container/ApiMonitoring/utils';
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
@@ -27,10 +30,12 @@ function EndPointDetails({
domainName,
endPointName,
setSelectedEndPointName,
domainListFilters,
}: {
domainName: string;
endPointName: string;
setSelectedEndPointName: (value: string) => void;
domainListFilters: IBuilderQuery['filters'];
}): JSX.Element {
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
@@ -101,8 +106,6 @@ function EndPointDetails({
const [
endPointMetricsDataQuery,
endPointStatusCodeDataQuery,
endPointRateOverTimeDataQuery,
endPointLatencyOverTimeDataQuery,
endPointDropDownDataQuery,
endPointDependentServicesDataQuery,
endPointStatusCodeBarChartsDataQuery,
@@ -115,12 +118,29 @@ function EndPointDetails({
endPointDetailsDataQueries[3],
endPointDetailsDataQueries[4],
endPointDetailsDataQueries[5],
endPointDetailsDataQueries[6],
endPointDetailsDataQueries[7],
],
[endPointDetailsDataQueries],
);
const { endpoint, port } = useMemo(
() => extractPortAndEndpoint(endPointName),
[endPointName],
);
const [rateOverTimeWidget, latencyOverTimeWidget] = useMemo(
() => [
getRateOverTimeWidgetData(domainName, endPointName, {
items: [...domainListFilters.items, ...filters.items],
op: filters.op,
}),
getLatencyOverTimeWidgetData(domainName, endPointName, {
items: [...domainListFilters.items, ...filters.items],
op: filters.op,
}),
],
[domainName, endPointName, filters, domainListFilters],
);
return (
<div className="endpoint-details-container">
<div className="endpoint-details-filters-container">
@@ -129,6 +149,8 @@ function EndPointDetails({
selectedEndPointName={endPointName}
setSelectedEndPointName={setSelectedEndPointName}
endPointDropDownDataQuery={endPointDropDownDataQuery}
parentContainerDiv=".endpoint-details-filters-container"
dropdownStyle={{ width: 'calc(100% - 36px)' }}
/>
</div>
<div className="endpoint-details-filters-container-search">
@@ -141,6 +163,16 @@ function EndPointDetails({
/>
</div>
</div>
<div className="endpoint-meta-data">
<div className="endpoint-meta-data-pill">
<div className="endpoint-meta-data-label">Endpoint</div>
<div className="endpoint-meta-data-value">{endpoint || '-'}</div>
</div>
<div className="endpoint-meta-data-pill">
<div className="endpoint-meta-data-label">Port</div>
<div className="endpoint-meta-data-value">{port || '-'}</div>
</div>
</div>
<EndPointMetrics endPointMetricsDataQuery={endPointMetricsDataQuery} />
{!isServicesFilterApplied && (
<DependentServices
@@ -152,18 +184,14 @@ function EndPointDetails({
endPointStatusCodeLatencyBarChartsDataQuery={
endPointStatusCodeLatencyBarChartsDataQuery
}
domainName={domainName}
endPointName={endPointName}
domainListFilters={domainListFilters}
filters={filters}
/>
<StatusCodeTable endPointStatusCodeDataQuery={endPointStatusCodeDataQuery} />
<MetricOverTimeGraph
metricOverTimeDataQuery={endPointRateOverTimeDataQuery}
widgetInfoIndex={0}
endPointName={endPointName}
/>
<MetricOverTimeGraph
metricOverTimeDataQuery={endPointLatencyOverTimeDataQuery}
widgetInfoIndex={1}
endPointName={endPointName}
/>
<MetricOverTimeGraph widget={rateOverTimeWidget} />
<MetricOverTimeGraph widget={latencyOverTimeWidget} />
</div>
);
}

View File

@@ -8,6 +8,7 @@ import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import EndPointDetailsZeroState from './components/EndPointDetailsZeroState';
@@ -17,10 +18,12 @@ function EndPointDetailsWrapper({
domainName,
endPointName,
setSelectedEndPointName,
domainListFilters,
}: {
domainName: string;
endPointName: string;
setSelectedEndPointName: (value: string) => void;
domainListFilters: IBuilderQuery['filters'];
}): JSX.Element {
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
@@ -69,6 +72,7 @@ function EndPointDetailsWrapper({
domainName={domainName}
endPointName={endPointName}
setSelectedEndPointName={setSelectedEndPointName}
domainListFilters={domainListFilters}
/>
);
}

View File

@@ -28,6 +28,8 @@ function EndPointDetailsZeroState({
<EndPointsDropDown
setSelectedEndPointName={setSelectedEndPointName}
endPointDropDownDataQuery={endPointDropDownDataQuery}
parentContainerDiv=".end-point-details-zero-state-wrapper"
dropdownStyle={{ width: '60%' }}
/>
</div>
</div>

View File

@@ -70,7 +70,7 @@ function EndPointMetrics({
<Skeleton.Button active size="small" />
) : (
<Tooltip title={metricsData?.rate}>
<span className="round-metric-tag">{metricsData?.rate}/sec</span>
<span className="round-metric-tag">{metricsData?.rate} ops/sec</span>
</Tooltip>
)}
</Typography.Text>

View File

@@ -8,16 +8,22 @@ interface EndPointsDropDownProps {
selectedEndPointName?: string;
setSelectedEndPointName: (value: string) => void;
endPointDropDownDataQuery: UseQueryResult<SuccessResponse<any>, unknown>;
parentContainerDiv?: string;
dropdownStyle?: React.CSSProperties;
}
const defaultProps = {
selectedEndPointName: '',
parentContainerDiv: '',
dropdownStyle: {},
};
function EndPointsDropDown({
selectedEndPointName,
setSelectedEndPointName,
endPointDropDownDataQuery,
parentContainerDiv,
dropdownStyle,
}: EndPointsDropDownProps): JSX.Element {
const { data, isLoading, isFetching } = endPointDropDownDataQuery;
@@ -39,6 +45,13 @@ function EndPointsDropDown({
style={{ width: '100%' }}
onChange={handleChange}
options={formattedData}
getPopupContainer={
parentContainerDiv
? (): HTMLElement =>
document.querySelector(parentContainerDiv) as HTMLElement
: (triggerNode): HTMLElement => triggerNode.parentNode as HTMLElement
}
dropdownStyle={dropdownStyle}
/>
);
}

View File

@@ -1,6 +1,7 @@
import { LoadingOutlined } from '@ant-design/icons';
import { Spin, Table } from 'antd';
import { ColumnType } from 'antd/lib/table';
import logEvent from 'api/common/logEvent';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import {
@@ -114,6 +115,7 @@ function ExpandedRow({
onClick: (): void => {
setSelectedEndPointName(record.endpointName);
setSelectedView(VIEW_TYPES.ENDPOINT_DETAILS);
logEvent('API Monitoring: Endpoint name row clicked', {});
},
className: 'expanded-clickable-row',
})}

View File

@@ -1,110 +1,18 @@
import { Card, Skeleton, Typography } from 'antd';
import cx from 'classnames';
import Uplot from 'components/Uplot';
import { PANEL_TYPES } from 'constants/queryBuilder';
import {
apiWidgetInfo,
extractPortAndEndpoint,
getFormattedChartData,
} from 'container/ApiMonitoring/utils';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { useCallback, useMemo, useRef } from 'react';
import { UseQueryResult } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { SuccessResponse } from 'types/api';
import { GlobalReducer } from 'types/reducer/globalTime';
import { Options } from 'uplot';
import ErrorState from './ErrorState';
function MetricOverTimeGraph({
metricOverTimeDataQuery,
widgetInfoIndex,
endPointName,
}: {
metricOverTimeDataQuery: UseQueryResult<SuccessResponse<any>, unknown>;
widgetInfoIndex: number;
endPointName: string;
}): JSX.Element {
const { data } = metricOverTimeDataQuery;
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const graphRef = useRef<HTMLDivElement>(null);
const dimensions = useResizeObserver(graphRef);
const { endpoint } = extractPortAndEndpoint(endPointName);
const formattedChartData = useMemo(
() => getFormattedChartData(data?.payload, [endpoint]),
[data?.payload, endpoint],
);
const chartData = useMemo(() => getUPlotChartData(formattedChartData), [
formattedChartData,
]);
const isDarkMode = useIsDarkMode();
const options = useMemo(
() =>
getUPlotChartOptions({
apiResponse: formattedChartData,
isDarkMode,
dimensions,
yAxisUnit: apiWidgetInfo[widgetInfoIndex].yAxisUnit,
softMax: null,
softMin: null,
minTimeScale: Math.floor(minTime / 1e9),
maxTimeScale: Math.floor(maxTime / 1e9),
panelType: PANEL_TYPES.TIME_SERIES,
}),
[
formattedChartData,
minTime,
maxTime,
widgetInfoIndex,
dimensions,
isDarkMode,
],
);
const renderCardContent = useCallback(
(query: UseQueryResult<SuccessResponse<any>, unknown>): JSX.Element => {
if (query.isLoading) {
return <Skeleton />;
}
if (query.error) {
return <ErrorState refetch={query.refetch} />;
}
return (
<div
className={cx('chart-container', {
'no-data-container':
!query.isLoading && !query?.data?.payload?.data?.result?.length,
})}
>
<Uplot options={options as Options} data={chartData} />
</div>
);
},
[options, chartData],
);
import { Card } from 'antd';
import GridCard from 'container/GridCardLayout/GridCard';
import { Widgets } from 'types/api/dashboard/getAll';
function MetricOverTimeGraph({ widget }: { widget: Widgets }): JSX.Element {
return (
<div>
<Card bordered className="endpoint-details-card">
<Typography.Text>{apiWidgetInfo[widgetInfoIndex].title}</Typography.Text>
<div className="graph-container" ref={graphRef}>
{renderCardContent(metricOverTimeDataQuery)}
<div className="graph-container">
<GridCard
widget={widget}
isQueryEnabled
onDragSelect={(): void => {}}
customOnDragSelect={(): void => {}}
/>
</div>
</Card>
</div>

View File

@@ -1,13 +1,22 @@
import { Color } from '@signozhq/design-tokens';
import { Button, Card, Skeleton, Typography } from 'antd';
import cx from 'classnames';
import { useGetGraphCustomSeries } from 'components/CeleryTask/useGetGraphCustomSeries';
import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplorer';
import Uplot from 'components/Uplot';
import { PANEL_TYPES } from 'constants/queryBuilder';
import {
createGroupByFiltersForBarChart,
getFormattedEndPointStatusCodeChartData,
getStatusCodeBarChartWidgetData,
statusCodeWidgetInfo,
} from 'container/ApiMonitoring/utils';
import { handleGraphClick } from 'container/GridCardLayout/GridCard/utils';
import { useGraphClickToShowButton } from 'container/GridCardLayout/useGraphClickToShowButton';
import useNavigateToExplorerPages from 'container/GridCardLayout/useNavigateToExplorerPages';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import { useNotifications } from 'hooks/useNotifications';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { useCallback, useMemo, useRef, useState } from 'react';
@@ -15,6 +24,8 @@ import { UseQueryResult } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { Options } from 'uplot';
@@ -23,6 +34,10 @@ import ErrorState from './ErrorState';
function StatusCodeBarCharts({
endPointStatusCodeBarChartsDataQuery,
endPointStatusCodeLatencyBarChartsDataQuery,
domainName,
endPointName,
domainListFilters,
filters,
}: {
endPointStatusCodeBarChartsDataQuery: UseQueryResult<
SuccessResponse<any>,
@@ -32,6 +47,10 @@ function StatusCodeBarCharts({
SuccessResponse<any>,
unknown
>;
domainName: string;
endPointName: string;
domainListFilters: IBuilderQuery['filters'];
filters: IBuilderQuery['filters'];
}): JSX.Element {
// 0 : Status Code Count
// 1 : Status Code Latency
@@ -85,6 +104,71 @@ function StatusCodeBarCharts({
const isDarkMode = useIsDarkMode();
const graphClick = useGraphClickToShowButton({
graphRef,
isButtonEnabled: true,
buttonClassName: 'view-onclick-show-button',
});
const navigateToExplorer = useNavigateToExplorer();
const navigateToExplorerPages = useNavigateToExplorerPages({
createGroupByFiltersUtil: createGroupByFiltersForBarChart,
});
const { notifications } = useNotifications();
const { getCustomSeries } = useGetGraphCustomSeries({
isDarkMode,
drawStyle: 'bars',
colorMapping: {
'200-299': Color.BG_FOREST_500,
'300-399': Color.BG_AMBER_400,
'400-499': Color.BG_CHERRY_500,
'500-599': Color.BG_ROBIN_500,
},
});
const widget = useMemo<Widgets>(
() =>
getStatusCodeBarChartWidgetData(domainName, endPointName, {
items: [...domainListFilters.items, ...filters.items],
op: filters.op,
}),
[domainName, endPointName, domainListFilters, filters],
);
const graphClickHandler = useCallback(
(
xValue: number,
yValue: number,
mouseX: number,
mouseY: number,
metric?: { [key: string]: string },
queryData?: { queryName: string; inFocusOrNot: boolean },
): void => {
handleGraphClick({
xValue,
yValue,
mouseX,
mouseY,
metric,
queryData,
widget,
navigateToExplorerPages,
navigateToExplorer,
notifications,
graphClick,
});
},
[
widget,
navigateToExplorerPages,
navigateToExplorer,
notifications,
graphClick,
],
);
const options = useMemo(
() =>
getUPlotChartOptions({
@@ -100,6 +184,8 @@ function StatusCodeBarCharts({
minTimeScale: Math.floor(minTime / 1e9),
maxTimeScale: Math.floor(maxTime / 1e9),
panelType: PANEL_TYPES.BAR,
onClickHandler: graphClickHandler,
customSeries: getCustomSeries,
}),
[
minTime,
@@ -109,6 +195,8 @@ function StatusCodeBarCharts({
formattedEndPointStatusCodeBarChartsDataPayload,
formattedEndPointStatusCodeLatencyBarChartsDataPayload,
isDarkMode,
graphClickHandler,
getCustomSeries,
],
);

View File

@@ -3,6 +3,7 @@ import '../Explorer.styles.scss';
import { LoadingOutlined } from '@ant-design/icons';
import { Spin, Table, Typography } from 'antd';
import axios from 'api';
import logEvent from 'api/common/logEvent';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import cx from 'classnames';
@@ -130,6 +131,7 @@ function DomainList({
(item) => item.key === record.key,
);
setSelectedDomainIndex(dataIndex);
logEvent('API Monitoring: Domain name row clicked', {});
}
},
className: 'expanded-clickable-row',
@@ -147,6 +149,7 @@ function DomainList({
handleClose={(): void => {
setSelectedDomainIndex(-1);
}}
domainListFilters={query?.filters}
/>
)}
</section>

View File

@@ -3,13 +3,14 @@ import './Explorer.styles.scss';
import { FilterOutlined } from '@ant-design/icons';
import * as Sentry from '@sentry/react';
import { Switch, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import { QuickFiltersSource } from 'components/QuickFilters/types';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
@@ -21,6 +22,10 @@ function Explorer(): JSX.Element {
const { currentQuery } = useQueryBuilder();
useEffect(() => {
logEvent('API Monitoring: Landing page visited', {});
}, []);
const { handleChangeQueryData } = useQueryOperations({
index: 0,
query: currentQuery.builder.queryData[0],
@@ -64,7 +69,12 @@ function Explorer(): JSX.Element {
style={{ marginLeft: 'auto' }}
checked={showIP}
onClick={(): void => {
setShowIP((showIP) => !showIP);
setShowIP((showIP): boolean => {
logEvent('API Monitoring: Show IP addresses clicked', {
showIP: !showIP,
});
return !showIP;
});
}}
/>
</div>

View File

@@ -8,16 +8,23 @@ import {
} from 'components/QuickFilters/types';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { GraphClickMetaData } from 'container/GridCardLayout/useNavigateToExplorerPages';
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
import dayjs from 'dayjs';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { cloneDeep } from 'lodash-es';
import { ArrowUpDown, ChevronDown, ChevronRight } from 'lucide-react';
import { getWidgetQuery } from 'pages/MessagingQueues/MQDetails/MetricPage/MetricPageUtil';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import {
BaseAutocompleteData,
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import {
IBuilderQuery,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import { QueryData } from 'types/api/widgets/getQuery';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
@@ -128,12 +135,15 @@ export const columnsConfig: ColumnType<APIDomainsRowData>[] = [
sorter: false,
align: 'right',
className: `column`,
render: (lastUsed: number): string => getLastUsedRelativeTime(lastUsed),
render: (lastUsed: number | string): string =>
lastUsed === 'n/a' || lastUsed === '-'
? '-'
: getLastUsedRelativeTime(lastUsed as number),
},
{
title: (
<div>
Rate <span className="round-metric-tag">/s</span>
Rate <span className="round-metric-tag">ops/s</span>
</div>
),
dataIndex: 'rate',
@@ -155,21 +165,26 @@ export const columnsConfig: ColumnType<APIDomainsRowData>[] = [
sorter: false,
align: 'right',
className: `column`,
render: (errorRate: number): React.ReactNode => (
<Progress
status="active"
percent={Number((errorRate * 100).toFixed(1))}
strokeLinecap="butt"
size="small"
strokeColor={((): string => {
const errorRatePercent = Number((errorRate * 100).toFixed(1));
if (errorRatePercent >= 90) return Color.BG_SAKURA_500;
if (errorRatePercent >= 60) return Color.BG_AMBER_500;
return Color.BG_FOREST_500;
})()}
className="progress-bar error-rate"
/>
),
render: (errorRate: number | string): React.ReactNode => {
if (errorRate === 'n/a' || errorRate === '-') {
return '-';
}
return (
<Progress
status="active"
percent={Number(((errorRate as number) * 100).toFixed(1))}
strokeLinecap="butt"
size="small"
strokeColor={((): string => {
const errorRatePercent = Number(((errorRate as number) * 100).toFixed(1));
if (errorRatePercent >= 90) return Color.BG_SAKURA_500;
if (errorRatePercent >= 60) return Color.BG_AMBER_500;
return Color.BG_FOREST_500;
})()}
className="progress-bar error-rate"
/>
);
},
},
{
title: (
@@ -217,9 +232,9 @@ interface APIMonitoringResponseRow {
data: {
endpoints: number;
error_rate: number;
lastseen: number;
lastseen: number | string;
[domainNameKey]: string;
p99: number;
p99: number | string;
rps: number;
};
}
@@ -232,12 +247,12 @@ interface EndPointsResponseRow {
export interface APIDomainsRowData {
key: string;
domainName: React.ReactNode;
endpointCount: React.ReactNode;
rate: React.ReactNode;
errorRate: React.ReactNode;
latency: React.ReactNode;
lastUsed: React.ReactNode;
domainName: string;
endpointCount: number | string;
rate: number | string;
errorRate: number | string;
latency: number | string;
lastUsed: string;
}
// Rename this to a proper name
@@ -246,12 +261,20 @@ export const formatDataForTable = (
): APIDomainsRowData[] =>
data?.map((domain) => ({
key: v4(),
domainName: domain.data[domainNameKey] || '',
endpointCount: domain.data.endpoints,
rate: domain.data.rps,
errorRate: domain.data.error_rate,
latency: Math.round(domain.data.p99 / 1000000), // Convert from nanoseconds to milliseconds
lastUsed: new Date(Math.floor(domain.data.lastseen / 1000000)).toISOString(), // Convert from nanoseconds to milliseconds
domainName: domain?.data[domainNameKey] || '-',
endpointCount: domain?.data?.endpoints || '-',
rate: domain.data.rps || '-',
errorRate: domain.data.error_rate || '-',
latency:
domain.data.p99 === 'n/a'
? '-'
: Math.round(Number(domain.data.p99) / 1000000), // Convert from nanoseconds to milliseconds
lastUsed:
domain.data.lastseen === 'n/a'
? '-'
: new Date(
Math.floor(Number(domain.data.lastseen) / 1000000),
).toISOString(), // Convert from nanoseconds to milliseconds
}));
// Rename this to a proper name
@@ -468,7 +491,6 @@ export const extractPortAndEndpoint = (
}
};
// Add icons in the below column headers
export const getEndPointsColumnsConfig = (
isGroupedByAttribute: boolean,
expandedRowKeys: React.Key[],
@@ -576,7 +598,7 @@ export const formatEndPointsDataForTable = (
);
return {
key: v4(),
endpointName: (endpoint.data['http.url'] as string) || '',
endpointName: (endpoint.data['http.url'] as string) || '-',
port,
callCount: endpoint.data.A || '-',
latency:
@@ -593,7 +615,6 @@ export const formatEndPointsDataForTable = (
const groupedByAttributeData = groupBy.map((attribute) => attribute.key);
// TODO: Use tags to show the concatenated attribute values
return data?.map((endpoint) => {
const newEndpointName = groupedByAttributeData
.map((attribute) => endpoint.data[attribute])
@@ -639,7 +660,7 @@ export const createFiltersForSelectedRowData = (
type: null,
},
op: '=',
value: groupedByMeta[key],
value: groupedByMeta[key] || '',
id: key,
})),
);
@@ -649,12 +670,10 @@ export const createFiltersForSelectedRowData = (
// First query payload for endpoint metrics
// Second query payload for endpoint status code
// Third query payload for endpoint rate over time graph
// Fourth query payload for endpoint latency over time graph
// Fifth query payload for endpoint dropdown selection
// Sixth query payload for endpoint dependant services
// Seventh query payload for endpoint response status count bar chart
// Eighth query payload for endpoint response status code latency bar chart
// Third query payload for endpoint dropdown selection
// Fourth query payload for endpoint dependant services
// Fifth query payload for endpoint response status count bar chart
// Sixth query payload for endpoint response status code latency bar chart
export const getEndPointDetailsQueryPayload = (
domainName: string,
endPointName: string,
@@ -1101,205 +1120,6 @@ export const getEndPointDetailsQueryPayload = (
end,
step: 60,
},
{
selectedTime: 'GLOBAL_TIME',
graphType: PANEL_TYPES.TIME_SERIES,
query: {
builder: {
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.String,
id: '------false',
isColumn: false,
key: '',
type: '',
},
aggregateOperator: 'rate',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'B',
filters: {
items: [
{
id: '3c76fe0b',
key: {
dataType: DataTypes.String,
id: 'net.peer.name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'net.peer.name',
type: 'tag',
},
op: '=',
value: domainName,
},
{
id: '30710f04',
key: {
dataType: DataTypes.String,
id: 'http.url--string--tag--false',
isColumn: false,
isJSON: false,
key: 'http.url',
type: 'tag',
},
op: '=',
value: endPointName,
},
...filters.items,
],
op: 'AND',
},
functions: [],
groupBy: [
{
dataType: DataTypes.String,
id: 'http.url--string--tag--false',
isColumn: false,
isJSON: false,
key: 'http.url',
type: 'tag',
},
],
having: [],
legend: '',
limit: null,
orderBy: [],
queryName: 'B',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'rate',
},
],
queryFormulas: [],
},
clickhouse_sql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
id: '315b15fa-ff0c-442f-89f8-2bf4fb1af2f2',
promql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
queryType: EQueryType.QUERY_BUILDER,
},
variables: {},
formatForWeb: false,
start,
end,
step: 60,
},
{
selectedTime: 'GLOBAL_TIME',
graphType: PANEL_TYPES.TIME_SERIES,
query: {
builder: {
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'duration_nano--float64----true',
isColumn: true,
isJSON: false,
key: 'duration_nano',
type: '',
},
aggregateOperator: 'p99',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'B',
filters: {
items: [
{
id: '63adb3ff',
key: {
dataType: DataTypes.String,
id: 'net.peer.name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'net.peer.name',
type: 'tag',
},
op: '=',
value: domainName,
},
{
id: '50142500',
key: {
dataType: DataTypes.String,
id: 'http.url--string--tag--false',
isColumn: false,
isJSON: false,
key: 'http.url',
type: 'tag',
},
op: '=',
value: endPointName,
},
...filters.items,
],
op: 'AND',
},
functions: [],
groupBy: [
{
dataType: DataTypes.String,
id: 'http.url--string--tag--false',
isColumn: false,
isJSON: false,
key: 'http.url',
type: 'tag',
},
],
having: [],
legend: '',
limit: null,
orderBy: [],
queryName: 'B',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'p99',
},
],
queryFormulas: [],
},
clickhouse_sql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
id: '315b15fa-ff0c-442f-89f8-2bf4fb1af2f2',
promql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
queryType: EQueryType.QUERY_BUILDER,
},
variables: {},
formatForWeb: false,
start,
end,
step: 60,
},
{
selectedTime: 'GLOBAL_TIME',
graphType: PANEL_TYPES.TABLE,
@@ -1801,7 +1621,7 @@ interface EndPointMetricsData {
interface EndPointStatusCodeData {
key: string;
statusCode: string;
count: number;
count: number | string;
p99Latency: number | string;
}
@@ -1824,8 +1644,8 @@ export const getFormattedEndPointStatusCodeData = (
): EndPointStatusCodeData[] =>
data?.map((row) => ({
key: v4(),
statusCode: row.data.response_status_code,
count: row.data.A,
statusCode: row.data.response_status_code || '-',
count: row.data.A || '-',
p99Latency:
row.data.B === 'n/a' ? '-' : Math.round(Number(row.data.B) / 1000000), // Convert from nanoseconds to milliseconds,
}));
@@ -1857,11 +1677,6 @@ export const endPointStatusCodeColumns: ColumnType<EndPointStatusCodeData>[] = [
},
];
export const apiWidgetInfo = [
{ title: 'Rate over time', yAxisUnit: 'ops/s' },
{ title: 'Latency over time', yAxisUnit: 'ns' },
];
export const statusCodeWidgetInfo = [
{ yAxisUnit: 'calls' },
{ yAxisUnit: 'ns' },
@@ -1885,8 +1700,8 @@ export const getFormattedEndPointDropDownData = (
): EndPointDropDownData[] =>
data?.map((row) => ({
key: v4(),
label: row.data['http.url'],
value: row.data['http.url'],
label: row.data['http.url'] || '-',
value: row.data['http.url'] || '-',
}));
interface DependentServicesResponseRow {
@@ -1903,6 +1718,7 @@ interface DependentServicesData {
percentage: number;
}
// Discuss once about type safety of this function
export const getFormattedDependentServicesData = (
data: DependentServicesResponseRow[],
): DependentServicesData[] => {
@@ -1983,7 +1799,7 @@ export const groupStatusCodes = (
// Track all timestamps
series.values.forEach((value) => {
allTimestamps.add(value[0]);
allTimestamps.add(Number(value[0]));
});
// Initialize or update the grouped series
@@ -2051,6 +1867,115 @@ export const groupStatusCodes = (
return Object.values(groupedSeries);
};
export const getStatusCodeBarChartWidgetData = (
domainName: string,
endPointName: string,
filters: IBuilderQuery['filters'],
): Widgets => ({
query: {
builder: {
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.String,
id: '------false',
isColumn: false,
key: '',
type: '',
},
aggregateOperator: 'count',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
filters: {
items: [
{
id: 'c6724407',
key: {
dataType: DataTypes.String,
id: 'net.peer.name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'net.peer.name',
type: 'tag',
},
op: '=',
value: domainName,
},
{
id: '8b1be6f0',
key: {
dataType: DataTypes.String,
id: 'http.url--string--tag--false',
isColumn: false,
isJSON: false,
key: 'http.url',
type: 'tag',
},
op: '=',
value: endPointName,
},
...filters.items,
],
op: 'AND',
},
functions: [],
groupBy: [
{
dataType: DataTypes.String,
id: 'response_status_code--string----true',
isColumn: true,
isJSON: false,
key: 'response_status_code',
type: '',
},
],
having: [],
legend: '',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'rate',
},
],
queryFormulas: [],
},
clickhouse_sql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
id: '315b15fa-ff0c-442f-89f8-2bf4fb1af2f2',
promql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
queryType: EQueryType.QUERY_BUILDER,
},
description: '',
id: '315b15fa-ff0c-442f-89f8-2bf4fb1af2f2',
isStacked: false,
panelTypes: PANEL_TYPES.BAR,
title: '',
opacity: '',
nullZeroValues: '',
timePreferance: 'GLOBAL_TIME',
softMin: null,
softMax: null,
selectedLogFields: null,
selectedTracesFields: null,
});
interface EndPointStatusCodePayloadData {
data: {
result: QueryData[];
@@ -2085,3 +2010,234 @@ export const END_POINT_DETAILS_QUERY_KEYS_ARRAY = [
REACT_QUERY_KEY.GET_ENDPOINT_STATUS_CODE_BAR_CHARTS_DATA,
REACT_QUERY_KEY.GET_ENDPOINT_STATUS_CODE_LATENCY_BAR_CHARTS_DATA,
];
export const getRateOverTimeWidgetData = (
domainName: string,
endPointName: string,
filters: IBuilderQuery['filters'],
): Widgets => {
const { endpoint, port } = extractPortAndEndpoint(endPointName);
const legend = `${
port !== '-' && port !== 'n/a' ? `${port}:` : ''
}${endpoint}`;
return getWidgetQueryBuilder(
getWidgetQuery({
title: 'Rate Over Time',
description: 'Rate over time.',
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.String,
id: '------false',
isColumn: false,
key: '',
type: '',
},
aggregateOperator: 'rate',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
filters: {
items: [
{
id: '3c76fe0b',
key: {
dataType: DataTypes.String,
id: 'net.peer.name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'net.peer.name',
type: 'tag',
},
op: '=',
value: domainName,
},
{
id: '30710f04',
key: {
dataType: DataTypes.String,
id: 'http.url--string--tag--false',
isColumn: false,
isJSON: false,
key: 'http.url',
type: 'tag',
},
op: '=',
value: endPointName,
},
...filters.items,
],
op: 'AND',
},
functions: [],
groupBy: [
{
dataType: DataTypes.String,
id: 'http.url--string--tag--false',
isColumn: false,
isJSON: false,
key: 'http.url',
type: 'tag',
},
],
having: [],
legend,
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'rate',
},
],
yAxisUnit: 'ops/s',
}),
);
};
export const getLatencyOverTimeWidgetData = (
domainName: string,
endPointName: string,
filters: IBuilderQuery['filters'],
): Widgets => {
const { endpoint, port } = extractPortAndEndpoint(endPointName);
const legend = `${port}:${endpoint}`;
return getWidgetQueryBuilder(
getWidgetQuery({
title: 'Latency Over Time',
description: 'Latency over time.',
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'duration_nano--float64----true',
isColumn: true,
isJSON: false,
key: 'duration_nano',
type: '',
},
aggregateOperator: 'p99',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
filters: {
items: [
{
id: '63adb3ff',
key: {
dataType: DataTypes.String,
id: 'net.peer.name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'net.peer.name',
type: 'tag',
},
op: '=',
value: domainName,
},
{
id: '50142500',
key: {
dataType: DataTypes.String,
id: 'http.url--string--tag--false',
isColumn: false,
isJSON: false,
key: 'http.url',
type: 'tag',
},
op: '=',
value: endPointName,
},
...filters.items,
],
op: 'AND',
},
functions: [],
groupBy: [
{
dataType: DataTypes.String,
id: 'http.url--string--tag--false',
isColumn: false,
isJSON: false,
key: 'http.url',
type: 'tag',
},
],
having: [],
legend,
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'p99',
},
],
yAxisUnit: 'ns',
}),
);
};
/**
* Helper function to get the start and end status codes from a status code range string
* @param value Status code range string (e.g. '200-299') or boolean
* @returns Tuple of [startStatusCode, endStatusCode] as strings
*/
const getStartAndEndStatusCode = (
value: string | boolean,
): [string, string] => {
if (!value) {
return ['', ''];
}
switch (value) {
case '100-199':
return ['100', '199'];
case '200-299':
return ['200', '299'];
case '300-399':
return ['300', '399'];
case '400-499':
return ['400', '499'];
case '500-599':
return ['500', '599'];
default:
return ['', ''];
}
};
/**
* Creates filter items for bar chart based on group by fields and request data
* Used specifically for filtering status code ranges in bar charts
* @param groupBy Array of group by fields to create filters for
* @param requestData Data from graph click containing values to filter on
* @returns Array of TagFilterItems with >= and < operators for status code ranges
*/
export const createGroupByFiltersForBarChart = (
groupBy: BaseAutocompleteData[],
requestData: GraphClickMetaData,
): TagFilterItem[] =>
groupBy
.map((gb) => {
const value = requestData[gb.key];
const [startStatusCode, endStatusCode] = getStartAndEndStatusCode(value);
return value
? [
{
id: v4(),
key: gb,
op: '>=',
value: startStatusCode,
},
{
id: v4(),
key: gb,
op: '<=',
value: endStatusCode,
},
]
: [];
})
.flat();

View File

@@ -49,6 +49,7 @@ function FullView({
isDependedDataLoaded = false,
onToggleModelHandler,
onClickHandler,
customOnDragSelect,
setCurrentGraphRef,
}: FullViewProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
@@ -252,7 +253,7 @@ function FullView({
onToggleModelHandler={onToggleModelHandler}
setGraphVisibility={setGraphsVisibilityStates}
graphVisibility={graphsVisibilityStates}
onDragSelect={onDragSelect}
onDragSelect={customOnDragSelect ?? onDragSelect}
tableProcessedDataRef={tableProcessedDataRef}
searchTerm={searchTerm}
onClickHandler={onClickHandler}

View File

@@ -50,6 +50,7 @@ export interface FullViewProps {
widget: Widgets;
fullViewOptions?: boolean;
onClickHandler?: OnClickPluginOpts['onClick'];
customOnDragSelect?: (start: number, end: number) => void;
name: string;
tableProcessedDataRef: MutableRefObject<RowData[]>;
version?: string;

View File

@@ -50,6 +50,7 @@ function WidgetGraphComponent({
setRequestData,
onClickHandler,
onDragSelect,
customOnDragSelect,
customTooltipElement,
openTracesButton,
onOpenTraceBtnClick,
@@ -327,6 +328,7 @@ function WidgetGraphComponent({
onToggleModelHandler={onToggleModelHandler}
tableProcessedDataRef={tableProcessedDataRef}
onClickHandler={onClickHandler ?? graphClickHandler}
customOnDragSelect={customOnDragSelect}
setCurrentGraphRef={setCurrentGraphRef}
/>
</Modal>

View File

@@ -36,6 +36,7 @@ function GridCardGraph({
version,
onClickHandler,
onDragSelect,
customOnDragSelect,
customTooltipElement,
dataAvailable,
getGraphData,
@@ -272,6 +273,7 @@ function GridCardGraph({
setRequestData={setRequestData}
onClickHandler={onClickHandler}
onDragSelect={onDragSelect}
customOnDragSelect={customOnDragSelect}
customTooltipElement={customTooltipElement}
openTracesButton={openTracesButton}
onOpenTraceBtnClick={onOpenTraceBtnClick}

View File

@@ -33,6 +33,7 @@ export interface WidgetGraphComponentProps {
setRequestData?: Dispatch<SetStateAction<GetQueryResultsProps>>;
onClickHandler?: OnClickPluginOpts['onClick'];
onDragSelect: (start: number, end: number) => void;
customOnDragSelect?: (start: number, end: number) => void;
customTooltipElement?: HTMLDivElement;
openTracesButton?: boolean;
onOpenTraceBtnClick?: (record: RowData) => void;
@@ -49,6 +50,7 @@ export interface GridCardGraphProps {
variables?: Dashboard['data']['variables'];
version?: string;
onDragSelect: (start: number, end: number) => void;
customOnDragSelect?: (start: number, end: number) => void;
customTooltipElement?: HTMLDivElement;
dataAvailable?: (isDataAvailable: boolean) => void;
getGraphData?: (graphData?: MetricRangePayloadProps['data']) => void;

View File

@@ -12,7 +12,7 @@ import { v4 } from 'uuid';
import { extractQueryNamesFromExpression } from './utils';
type GraphClickMetaData = {
export type GraphClickMetaData = {
[key: string]: string | boolean;
queryName: string;
inFocusOrNot: boolean;
@@ -66,6 +66,10 @@ const buildQueryFilters = (
export const buildFilters = (
query: Query,
requestData?: GraphClickMetaData,
createGroupByFiltersUtil?: (
groupBy: BaseAutocompleteData[],
requestData: GraphClickMetaData,
) => TagFilterItem[],
): {
[queryName: string]: { filters: TagFilterItem[]; dataSource?: string };
} => {
@@ -77,7 +81,9 @@ export const buildFilters = (
// Direct query match
if (queryData) {
const groupByFilters = createGroupByFilters(queryData.groupBy, requestData);
const groupByFilters = createGroupByFiltersUtil
? createGroupByFiltersUtil(queryData.groupBy, requestData)
: createGroupByFilters(queryData.groupBy, requestData);
return {
[requestData.queryName]: buildQueryFilters(queryData, groupByFilters),
};
@@ -100,7 +106,9 @@ export const buildFilters = (
} = {};
filteredQueryData.forEach((q) => {
const groupByFilters = createGroupByFilters(q.groupBy, requestData);
const groupByFilters = createGroupByFiltersUtil
? createGroupByFiltersUtil(q.groupBy, requestData)
: createGroupByFilters(q.groupBy, requestData);
returnObject[q.queryName] = buildQueryFilters(q, groupByFilters);
});
@@ -114,13 +122,19 @@ export const buildFilters = (
* Custom hook for handling navigation to explorer pages with query data
* @returns A function to handle navigation with query processing
*/
function useNavigateToExplorerPages(): (
function useNavigateToExplorerPages(props?: {
createGroupByFiltersUtil?: (
groupBy: BaseAutocompleteData[],
requestData: GraphClickMetaData,
) => TagFilterItem[];
}): (
props: NavigateToExplorerPagesProps,
) => Promise<{
[queryName: string]: { filters: TagFilterItem[]; dataSource?: string };
}> {
const { selectedDashboard } = useDashboard();
const { notifications } = useNotifications();
const { createGroupByFiltersUtil } = props ?? {};
return useCallback(
async ({ widget, requestData }: NavigateToExplorerPagesProps) => {
@@ -129,6 +143,7 @@ function useNavigateToExplorerPages(): (
return buildFilters(
widget.query,
requestData ?? { queryName: '', inFocusOrNot: false },
createGroupByFiltersUtil,
);
} catch (error) {
notifications.error({

View File

@@ -279,6 +279,17 @@ function SideNav(): JSX.Element {
let updatedUserManagementItems: UserManagementMenuItems[] = [
manageLicenseMenuItem,
];
const isApiMonitoringEnabled = featureFlags?.find(
(flag) => flag.name === FeatureKeys.THIRD_PARTY_API,
)?.active;
if (!isApiMonitoringEnabled) {
updatedMenuItems = updatedMenuItems.filter(
(item) => item.key !== ROUTES.API_MONITORING,
);
}
if (isCloudUserVal || isEECloudUserVal) {
const isOnboardingEnabled =
featureFlags?.find((feature) => feature.name === FeatureKeys.ONBOARDING)

View File

@@ -128,6 +128,7 @@ const menuItems: SidebarItem[] = [
key: ROUTES.API_MONITORING,
label: 'API Monitoring',
icon: <Binoculars size={16} />,
isNew: true,
},
{
key: ROUTES.LIST_ALL_ALERT,

View File

@@ -1,211 +0,0 @@
import './styles.scss';
import { Button, Card, Col, Divider, Row, Switch, Typography } from 'antd';
import MultiSelect, {
MultiSelectOption,
MultiSelectSection,
} from 'components/MultiSelect';
import { useState } from 'react';
const { Title, Text, Paragraph } = Typography;
// Sample data for the component
const sampleOptions: MultiSelectOption[] = [
{ label: 'abc', value: 'abc' },
{ label: 'acbewc', value: 'acbewc' },
{ label: 'custom-value', value: 'custom-value' },
{ label: 'option1', value: 'option1' },
{ label: 'option2', value: 'option2' },
{ label: 'another-option', value: 'another-option' },
{ label: 'test-option', value: 'test-option' },
{ label: 'disabled-option', value: 'disabled-option', disabled: true },
];
// Sample related values for the "Related Values" section
const relatedValues: MultiSelectOption[] = [
{ label: 'gke-mgmt-pl-generator-e2st4-sp-f1c1bde8-skbl', value: 'gke-1' },
{ label: 'gke-mgmt-pl-generator-e2st4-sp-f1c1bde8-skb2', value: 'gke-2' },
{ label: 'gke-mgmt-pl-generator-e2st4-sp-f1c1bde8-skb3', value: 'gke-3' },
];
// Sample all values for the "All Values" section
const allValues: MultiSelectOption[] = Array.from({ length: 20 }, (_, i) => ({
label: `gke-mgmt-pl-generator-e2st4-sp-f1c1bde8-7a7w-${i + 1}`,
value: `all-${i + 1}`,
}));
// Creating sections
const sections: MultiSelectSection[] = [
{
title: 'Related Values',
options: relatedValues,
},
{
title: 'ALL Values',
options: allValues,
},
];
function DynamicVariableTestPage(): JSX.Element {
const [selectedValues, setSelectedValues] = useState<string[]>([
'abc',
'acbewc',
]);
const [loadingDemo, setLoadingDemo] = useState<boolean>(false);
const [allowCustom, setAllowCustom] = useState<boolean>(true);
const [showError, setShowError] = useState<boolean>(false);
const [disabled, setDisabled] = useState<boolean>(false);
const handleChange = (values: string[]): void => {
setSelectedValues(values);
};
const toggleLoading = (): void => {
setLoadingDemo((prev) => !prev);
};
return (
<div className="dynamic-variable-page">
<Card>
<Title level={3}>Dynamic Variable MultiSelect</Title>
<Paragraph>
This page demonstrates the MultiSelect component with various features. The
component is now fully reusable and production-ready with support for
dynamic data from APIs, proper error states, accessibility, and UI
improvements.
</Paragraph>
<Divider />
<Row gutter={[16, 16]}>
<Col xs={24} md={12}>
<Title level={5}>Basic MultiSelect</Title>
<Text>This example shows the basic usage with pre-selected values.</Text>
<div className="multiselect-demo-container">
<MultiSelect
options={sampleOptions}
value={selectedValues}
onChange={handleChange}
placeholder="Search or add values..."
label="Select options"
/>
</div>
<div className="selected-values-display">
<Title level={5}>Selected Values:</Title>
<pre>{JSON.stringify(selectedValues, null, 2)}</pre>
</div>
</Col>
<Col xs={24} md={12}>
<Title level={5}>With Sections & All Values</Title>
<Text>This example shows the component with additional sections.</Text>
<div className="multiselect-demo-container">
<MultiSelect
options={sampleOptions}
value={selectedValues}
onChange={handleChange}
placeholder="Search or add values..."
additionalSections={sections}
sectionMaxHeight={120}
/>
</div>
</Col>
</Row>
<Divider />
<Row gutter={[16, 16]}>
<Col xs={24} md={12}>
<Title level={5}>Loading State</Title>
<Text>This example demonstrates the loading state.</Text>
<div className="multiselect-demo-container">
<MultiSelect
options={sampleOptions}
value={[]}
onChange={(): void => {}}
loading={loadingDemo}
placeholder="This shows loading state..."
/>
<Button onClick={toggleLoading} style={{ marginTop: 16 }}>
{loadingDemo ? 'Stop Loading' : 'Simulate Loading'}
</Button>
</div>
</Col>
<Col xs={24} md={12}>
<Title level={5}>Custom Values Configuration</Title>
<Text>Toggle to enable/disable custom values.</Text>
<div className="multiselect-demo-container">
<MultiSelect
options={sampleOptions}
value={[]}
onChange={(): void => {}}
allowCustomValues={allowCustom}
placeholder={
allowCustom ? 'Custom values allowed...' : 'Only predefined values...'
}
/>
<div style={{ marginTop: 16 }}>
<Switch
checked={allowCustom}
onChange={setAllowCustom}
checkedChildren="Custom values on"
unCheckedChildren="Custom values off"
/>
</div>
</div>
</Col>
</Row>
<Divider />
<Row gutter={[16, 16]}>
<Col xs={24} md={12}>
<Title level={5}>Error State</Title>
<Text>This example shows the component with an error.</Text>
<div className="multiselect-demo-container">
<MultiSelect
options={sampleOptions}
value={[]}
onChange={(): void => {}}
placeholder="Select some options..."
error={showError ? 'Please select at least one option' : undefined}
/>
<Button
onClick={(): void => setShowError(!showError)}
style={{ marginTop: 16 }}
type={showError ? 'primary' : 'default'}
>
{showError ? 'Hide Error' : 'Show Error'}
</Button>
</div>
</Col>
<Col xs={24} md={12}>
<Title level={5}>Disabled State</Title>
<Text>This example shows the disabled state of the component.</Text>
<div className="multiselect-demo-container">
<MultiSelect
options={sampleOptions}
value={['abc', 'option1']}
onChange={(): void => {}}
placeholder="This component is disabled..."
disabled={disabled}
/>
<div style={{ marginTop: 16 }}>
<Switch
checked={disabled}
onChange={setDisabled}
checkedChildren="Disabled"
unCheckedChildren="Enabled"
/>
</div>
</div>
</Col>
</Row>
</Card>
</div>
);
}
export default DynamicVariableTestPage;

View File

@@ -1,20 +0,0 @@
.dynamic-variable-page {
padding: 24px;
max-width: 800px;
margin: 0 auto;
}
.multiselect-demo-container {
margin-top: 16px;
width: 100%;
}
.selected-values-display {
margin-top: 24px;
pre {
background-color: #f5f5f5;
padding: 16px;
border-radius: 6px;
}
}

View File

@@ -120,5 +120,4 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
API_MONITORING: ['ADMIN', 'EDITOR', 'VIEWER'],
WORKSPACE_ACCESS_RESTRICTED: ['ADMIN', 'EDITOR', 'VIEWER'],
METRICS_EXPLORER_BASE: ['ADMIN', 'EDITOR', 'VIEWER'],
DYNAMIC_VARIABLE_TEST: ['ADMIN', 'EDITOR', 'VIEWER'],
};

View File

@@ -20,7 +20,6 @@ import (
smtpservice "github.com/SigNoz/signoz/pkg/query-service/utils/smtpService"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
)
@@ -88,18 +87,12 @@ func Invite(ctx context.Context, req *model.InviteRequest) (*model.InviteRespons
}
inv := &types.Invite{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
TimeAuditable: types.TimeAuditable{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
Name: req.Name,
Email: req.Email,
Token: token,
Role: req.Role,
OrgID: au.OrgID,
Name: req.Name,
Email: req.Email,
Token: token,
CreatedAt: time.Now(),
Role: req.Role,
OrgID: au.OrgID,
}
if err := dao.DB().CreateInviteEntry(ctx, inv); err != nil {
@@ -195,18 +188,12 @@ func inviteUser(ctx context.Context, req *model.InviteRequest, au *types.Gettabl
}
inv := &types.Invite{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
TimeAuditable: types.TimeAuditable{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
Name: req.Name,
Email: req.Email,
Token: token,
Role: req.Role,
OrgID: au.OrgID,
Name: req.Name,
Email: req.Email,
Token: token,
CreatedAt: time.Now(),
Role: req.Role,
OrgID: au.OrgID,
}
if err := dao.DB().CreateInviteEntry(ctx, inv); err != nil {

View File

@@ -63,7 +63,6 @@ func NewSQLMigrationProviderFactories(sqlstore sqlstore.SQLStore) factory.NamedM
sqlmigration.NewUpdatePatAndOrgDomainsFactory(sqlstore),
sqlmigration.NewUpdatePipelines(sqlstore),
sqlmigration.NewDropLicensesSitesFactory(sqlstore),
sqlmigration.NewUpdateInvitesFactory(sqlstore),
)
}

View File

@@ -1,139 +0,0 @@
package sqlmigration
import (
"context"
"database/sql"
"time"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
)
type updateInvites struct {
store sqlstore.SQLStore
}
type existingInvite struct {
bun.BaseModel `bun:"table:invites"`
OrgID string `bun:"org_id,type:text,notnull" json:"orgId"`
ID int `bun:"id,pk,autoincrement" json:"id"`
Name string `bun:"name,type:text,notnull" json:"name"`
Email string `bun:"email,type:text,notnull,unique" json:"email"`
Token string `bun:"token,type:text,notnull" json:"token"`
CreatedAt time.Time `bun:"created_at,notnull" json:"createdAt"`
Role string `bun:"role,type:text,notnull" json:"role"`
}
type newInvite struct {
bun.BaseModel `bun:"table:user_invite"`
types.Identifiable
types.TimeAuditable
Name string `bun:"name,type:text,notnull" json:"name"`
Email string `bun:"email,type:text,notnull,unique" json:"email"`
Token string `bun:"token,type:text,notnull" json:"token"`
Role string `bun:"role,type:text,notnull" json:"role"`
OrgID string `bun:"org_id,type:text,notnull" json:"orgId"`
}
func NewUpdateInvitesFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
return factory.
NewProviderFactory(
factory.MustNewName("update_invites"),
func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return newUpdateInvites(ctx, ps, c, sqlstore)
})
}
func newUpdateInvites(_ context.Context, _ factory.ProviderSettings, _ Config, store sqlstore.SQLStore) (SQLMigration, error) {
return &updateInvites{store: store}, nil
}
func (migration *updateInvites) Register(migrations *migrate.Migrations) error {
if err := migrations.
Register(migration.Up, migration.Down); err != nil {
return err
}
return nil
}
func (migration *updateInvites) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.
BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
err = migration.
store.
Dialect().
RenameTableAndModifyModel(ctx, tx, new(existingInvite), new(newInvite), func(ctx context.Context) error {
existingInvites := make([]*existingInvite, 0)
err = tx.
NewSelect().
Model(&existingInvites).
Scan(ctx)
if err != nil {
if err != sql.ErrNoRows {
return err
}
}
if err == nil && len(existingInvites) > 0 {
newInvites := migration.
CopyOldInvitesToNewInvites(existingInvites)
_, err = tx.
NewInsert().
Model(&newInvites).
Exec(ctx)
if err != nil {
return err
}
}
return nil
})
if err != nil {
return err
}
err = tx.Commit()
if err != nil {
return err
}
return nil
}
func (migration *updateInvites) Down(context.Context, *bun.DB) error {
return nil
}
func (migration *updateInvites) CopyOldInvitesToNewInvites(existingInvites []*existingInvite) []*newInvite {
newInvites := make([]*newInvite, 0)
for _, invite := range existingInvites {
newInvites = append(newInvites, &newInvite{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
TimeAuditable: types.TimeAuditable{
CreatedAt: invite.CreatedAt,
UpdatedAt: time.Now(),
},
Name: invite.Name,
Email: invite.Email,
Token: invite.Token,
Role: invite.Role,
OrgID: invite.OrgID,
})
}
return newInvites
}

View File

@@ -2,7 +2,6 @@ package postgressqlstore
import (
"context"
"reflect"
"github.com/uptrace/bun"
)
@@ -152,60 +151,3 @@ func (dialect *dialect) RenameColumn(ctx context.Context, bun bun.IDB, table str
}
return true, nil
}
func (dialect *dialect) TableExists(ctx context.Context, bun bun.IDB, table interface{}) (bool, error) {
count := 0
err := bun.
NewSelect().
ColumnExpr("count(*)").
Table("pg_catalog.pg_tables").
Where("tablename = ?", bun.Dialect().Tables().Get(reflect.TypeOf(table)).Name).
Scan(ctx, &count)
if err != nil {
return false, err
}
if count == 0 {
return false, nil
}
return true, nil
}
func (dialect *dialect) RenameTableAndModifyModel(ctx context.Context, bun bun.IDB, oldModel interface{}, newModel interface{}, cb func(context.Context) error) error {
exists, err := dialect.TableExists(ctx, bun, newModel)
if err != nil {
return err
}
if exists {
return nil
}
_, err = bun.
NewCreateTable().
IfNotExists().
Model(newModel).
Exec(ctx)
if err != nil {
return err
}
err = cb(ctx)
if err != nil {
return err
}
_, err = bun.
NewDropTable().
IfExists().
Model(oldModel).
Exec(ctx)
if err != nil {
return err
}
return nil
}

View File

@@ -2,7 +2,6 @@ package sqlitesqlstore
import (
"context"
"reflect"
"github.com/uptrace/bun"
)
@@ -142,62 +141,3 @@ func (dialect *dialect) RenameColumn(ctx context.Context, bun bun.IDB, table str
}
return true, nil
}
func (dialect *dialect) TableExists(ctx context.Context, bun bun.IDB, table interface{}) (bool, error) {
count := 0
err := bun.
NewSelect().
ColumnExpr("count(*)").
Table("sqlite_master").
Where("type = ?", "table").
Where("name = ?", bun.Dialect().Tables().Get(reflect.TypeOf(table)).Name).
Scan(ctx, &count)
if err != nil {
return false, err
}
if count == 0 {
return false, nil
}
return true, nil
}
func (dialect *dialect) RenameTableAndModifyModel(ctx context.Context, bun bun.IDB, oldModel interface{}, newModel interface{}, cb func(context.Context) error) error {
exists, err := dialect.TableExists(ctx, bun, newModel)
if err != nil {
return err
}
if exists {
return nil
}
_, err = bun.
NewCreateTable().
IfNotExists().
Model(newModel).
ForeignKey(`("org_id") REFERENCES "organizations" ("id")`).
Exec(ctx)
if err != nil {
return err
}
err = cb(ctx)
if err != nil {
return err
}
_, err = bun.
NewDropTable().
IfExists().
Model(oldModel).
Exec(ctx)
if err != nil {
return err
}
return nil
}

View File

@@ -42,5 +42,4 @@ type SQLDialect interface {
GetColumnType(context.Context, bun.IDB, string, string) (string, error)
ColumnExists(context.Context, bun.IDB, string, string) (bool, error)
RenameColumn(context.Context, bun.IDB, string, string, string) (bool, error)
RenameTableAndModifyModel(context.Context, bun.IDB, interface{}, interface{}, func(context.Context) error) error
}

View File

@@ -28,7 +28,3 @@ func (dialect *dialect) ColumnExists(ctx context.Context, bun bun.IDB, table str
func (dialect *dialect) RenameColumn(ctx context.Context, bun bun.IDB, table string, oldColumnName string, newColumnName string) (bool, error) {
return true, nil
}
func (dialect *dialect) RenameTableAndModifyModel(ctx context.Context, bun bun.IDB, oldModel interface{}, newModel interface{}, cb func(context.Context) error) error {
return nil
}

View File

@@ -1,19 +1,21 @@
package types
import (
"time"
"github.com/uptrace/bun"
)
type Invite struct {
bun.BaseModel `bun:"table:user_invite"`
bun.BaseModel `bun:"table:invites"`
Identifiable
TimeAuditable
OrgID string `bun:"org_id,type:text,notnull" json:"orgId"`
Name string `bun:"name,type:text,notnull" json:"name"`
Email string `bun:"email,type:text,notnull,unique" json:"email"`
Token string `bun:"token,type:text,notnull" json:"token"`
Role string `bun:"role,type:text,notnull" json:"role"`
OrgID string `bun:"org_id,type:text,notnull" json:"orgId"`
ID int `bun:"id,pk,autoincrement" json:"id"`
Name string `bun:"name,type:text,notnull" json:"name"`
Email string `bun:"email,type:text,notnull,unique" json:"email"`
Token string `bun:"token,type:text,notnull" json:"token"`
CreatedAt time.Time `bun:"created_at,notnull" json:"createdAt"`
Role string `bun:"role,type:text,notnull" json:"role"`
}
type Group struct {