Compare commits

...

1 Commits

Author SHA1 Message Date
Gaurav Tewari
8d0eda4a0d feat: new ui update 2026-08-17 10:56:46 +05:30
14 changed files with 149 additions and 244 deletions

View File

@@ -8,6 +8,10 @@
color: var(--l3-foreground);
}
.stateText {
--typography-font-size: var(--font-size-xs);
}
.stateContent {
display: inline-flex;
align-items: center;
@@ -24,8 +28,8 @@
}
.stateCell {
padding: var(--spacing-4) var(--spacing-6) var(--spacing-4) var(--spacing-12);
font-size: var(--periscope-font-size-base);
padding: var(--spacing-3) var(--spacing-6) var(--spacing-3) var(--spacing-12);
font-size: var(--font-size-xs);
}
.addMapperCell {

View File

@@ -82,8 +82,8 @@ function GroupMappers({
data-testid={`mappers-error-${group.localId}`}
>
<span className={styles.stateContent}>
<TriangleAlert size={14} color="var(--danger-background)" />
<Typography.Text as="span" size="base">
<TriangleAlert size={12} color="var(--danger-background)" />
<Typography.Text as="span" className={styles.stateText}>
Failed to load mappings. Please try again.
</Typography.Text>
</span>
@@ -103,8 +103,8 @@ function GroupMappers({
data-testid={`mappers-empty-${group.localId}`}
>
<span className={styles.stateContent}>
<ArrowLeftRight size={14} />
<Typography.Text as="span" size="base" color="muted">
<ArrowLeftRight size={12} />
<Typography.Text as="span" className={styles.stateText} color="muted">
No mappings in this group yet.
</Typography.Text>
</span>
@@ -123,7 +123,7 @@ function GroupMappers({
variant="link"
color="primary"
size="sm"
prefix={<Plus size={14} />}
prefix={<Plus size={12} />}
onClick={(): void => onAddMapper(group.localId)}
testId={`add-mapper-${group.localId}`}
>

View File

@@ -44,7 +44,7 @@ function MapperActionsMenu({
aria-label="Mapping actions"
testId={`mapper-actions-${mapper.localId}`}
>
<EllipsisVertical size={16} />
<EllipsisVertical size={14} />
</Button>
</DropdownMenuSimple>
);

View File

@@ -4,11 +4,13 @@
}
}
// Rows sit one level below the group header, so they run at a smaller type
// scale and tighter vertical rhythm than the header banner above them.
.cell {
padding: var(--spacing-4) var(--spacing-6);
vertical-align: middle;
color: var(--l1-foreground);
font-size: var(--periscope-font-size-base);
font-size: var(--font-size-xs);
}
.targetCell {
@@ -21,6 +23,13 @@
.targetName {
min-width: 0;
--typography-font-size: var(--font-size-xs);
}
.rowSwitch {
--switch-width: 1.5rem;
--switch-height: 0.875rem;
--switch-thumb-size: 0.625rem;
}
.targetContextBadge {

View File

@@ -49,7 +49,7 @@ function MapperRow({
>
<td className={cx(styles.cell, styles.targetCell)}>
<Typography.Text
weight="semibold"
weight="medium"
truncate={1}
title={mapper.name}
className={styles.targetName}
@@ -60,7 +60,7 @@ function MapperRow({
<Badge
color={
mapper.fieldContext === SpantypesFieldContextDTO.resource
? 'amber'
? 'forest'
: 'robin'
}
variant="outline"
@@ -106,6 +106,7 @@ function MapperRow({
{canManage && (
<div className={styles.rowActions}>
<Switch
className={styles.rowSwitch}
value={mapper.enabled}
onChange={(checked): void => onToggle(mapper.localId, checked)}
testId={`mapper-enabled-${mapper.localId}`}

View File

@@ -53,6 +53,7 @@
gap: var(--spacing-3);
background: var(--l2-background);
border-radius: 0;
border-left: 2px solid var(--accent-primary);
padding: var(--spacing-3) var(--spacing-6);
color: var(--l3-foreground);
@@ -112,6 +113,7 @@
padding: var(--spacing-3) var(--spacing-6);
background: var(--l2-background);
border-top: 1px solid var(--l2-border);
border-left: 2px solid var(--accent-primary);
&:last-child {
border-bottom: 1px solid var(--l2-border);

View File

@@ -5,7 +5,7 @@ import { Switch } from '@signozhq/ui/switch';
import ConditionKeyList from './components/ConditionKeyList/ConditionKeyList';
import styles from './GroupFormDrawer.module.scss';
import { FieldContext, GroupDraft, MapperDraftMode } from '../../types';
import { GroupDraft, MapperDraftMode } from '../../types';
import { isGroupDraftValid } from '../../utils';
interface GroupFormDrawerProps {
@@ -91,7 +91,6 @@ function GroupFormDrawer({
placeholder="e.g. gen_ai."
addLabel="Add attribute key"
testIdPrefix="group-form-attribute"
fieldContext={FieldContext.attribute}
onChange={(attributes): void => setDraft({ ...draft, attributes })}
/>
@@ -102,7 +101,6 @@ function GroupFormDrawer({
placeholder="e.g. service.name"
addLabel="Add resource key"
testIdPrefix="group-form-resource"
fieldContext={FieldContext.resource}
onChange={(resource): void => setDraft({ ...draft, resource })}
/>

View File

@@ -18,15 +18,52 @@
letter-spacing: normal;
}
.keys {
.keyBox {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
padding: var(--spacing-6);
border-radius: 6px;
border: 1px solid var(--l2-border);
}
.keyRow {
.keyChips {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-3);
}
.keyChip {
display: inline-flex;
align-items: center;
gap: var(--spacing-2);
font-family: var(--font-mono, monospace);
}
.keyChipText {
display: block;
max-width: 260px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.keyChipRemove {
display: inline-flex;
align-items: center;
padding: 0;
background: transparent;
border: none;
color: inherit;
cursor: pointer;
&:hover {
color: var(--accent-cherry);
}
}
.keyAdd {
display: flex;
gap: var(--spacing-3);
}

View File

@@ -1,8 +1,9 @@
import { useState } from 'react';
import { Badge } from '@signozhq/ui/badge';
import { Button } from '@signozhq/ui/button';
import { Plus, X } from '@signozhq/icons';
import { Input } from '@signozhq/ui/input';
import { X } from '@signozhq/icons';
import { FieldContextValue } from 'container/LLMObservability/AttributeMapping/types';
import KeySearchInput from '../../../KeySearchInput/KeySearchInput';
import styles from './ConditionKeyList.module.scss';
interface ConditionKeyListProps {
@@ -12,10 +13,11 @@ interface ConditionKeyListProps {
placeholder: string;
addLabel: string;
testIdPrefix: string;
fieldContext: FieldContextValue;
onChange: (keys: string[]) => void;
}
// Condition keys as removable chips. Chips are starred on both sides because
// the key is matched as a substring, not as a full key name.
function ConditionKeyList({
label,
labelHint,
@@ -23,19 +25,22 @@ function ConditionKeyList({
placeholder,
addLabel,
testIdPrefix,
fieldContext,
onChange,
}: ConditionKeyListProps): JSX.Element {
const updateKey = (index: number, value: string): void => {
onChange(keys.map((key, i) => (i === index ? value : key)));
};
const [keyInput, setKeyInput] = useState<string>('');
const addKey = (): void => {
onChange([...keys, '']);
const next = keyInput.trim();
if (!next || keys.includes(next)) {
setKeyInput('');
return;
}
onChange([...keys, next]);
setKeyInput('');
};
const removeKey = (index: number): void => {
onChange(keys.filter((_, i) => i !== index));
const removeKey = (key: string): void => {
onChange(keys.filter((existing) => existing !== key));
};
return (
@@ -45,43 +50,60 @@ function ConditionKeyList({
{labelHint && <span className={styles.labelHint}> {labelHint}</span>}
</span>
{keys.length > 0 && (
<div className={styles.keys}>
{keys.map((key, index) => (
// eslint-disable-next-line react/no-array-index-key
<div className={styles.keyRow} key={index}>
<KeySearchInput
className={styles.keyInput}
placeholder={placeholder}
value={key}
fieldContext={fieldContext}
onChange={(next): void => updateKey(index, next)}
testId={`${testIdPrefix}-${index}`}
/>
<Button
variant="ghost"
color="secondary"
size="icon"
aria-label="Remove key"
onClick={(): void => removeKey(index)}
testId={`${testIdPrefix}-remove-${index}`}
<div className={styles.keyBox}>
{keys.length > 0 && (
<div className={styles.keyChips} data-testid={`${testIdPrefix}-chips`}>
{keys.map((key) => (
<Badge
key={key}
color="vanilla"
variant="outline"
className={styles.keyChip}
testId={`${testIdPrefix}-chip-${key}`}
>
<X size={14} />
</Button>
</div>
))}
</div>
)}
<span className={styles.keyChipText} title={key}>
{`*${key}*`}
</span>
<button
type="button"
aria-label={`Remove ${key}`}
className={styles.keyChipRemove}
onClick={(): void => removeKey(key)}
data-testid={`${testIdPrefix}-remove-${key}`}
>
<X size={10} />
</button>
</Badge>
))}
</div>
)}
<Button
variant="dashed"
color="secondary"
prefix={<Plus size={14} />}
onClick={addKey}
testId={`${testIdPrefix}-add`}
>
{addLabel}
</Button>
<div className={styles.keyAdd}>
<Input
className={styles.keyInput}
placeholder={placeholder}
value={keyInput}
autoComplete="off"
onChange={(event): void => setKeyInput(event.target.value)}
onKeyDown={(event): void => {
if (event.key === 'Enter') {
event.preventDefault();
addKey();
}
}}
testId={`${testIdPrefix}-input`}
/>
<Button
variant="outlined"
color="secondary"
aria-label={addLabel}
onClick={addKey}
testId={`${testIdPrefix}-add`}
>
+ Add
</Button>
</div>
</div>
</div>
);
}

View File

@@ -1,49 +0,0 @@
.keySearch {
position: relative;
flex: 1;
min-width: 0;
}
.keySearchDropdown {
position: absolute;
top: calc(100% + var(--spacing-2));
left: 0;
z-index: 20;
width: 100%;
max-height: 240px;
overflow-x: hidden;
overflow-y: auto;
padding: var(--spacing-2);
border: 1px solid var(--l2-border);
border-radius: 6px;
background: var(--l2-background);
box-shadow: 0 8px 24px
color-mix(in srgb, var(--bg-base-black) 40%, transparent);
}
.keySearchDropdownEmpty {
padding: var(--spacing-4) var(--spacing-5);
font-size: var(--font-size-xs);
color: var(--l3-foreground);
}
.keySearchOption {
display: block;
width: 100%;
max-width: 100%;
padding: var(--spacing-3) var(--spacing-5);
border: none;
border-radius: 3px;
background: transparent;
color: var(--l1-foreground);
font-size: var(--font-size-xs);
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
&:hover {
background: var(--l2-background-hover);
}
}

View File

@@ -1,121 +0,0 @@
import { useMemo, useState } from 'react';
import { Input } from '@signozhq/ui/input';
import { useGetFieldsKeys } from 'api/generated/services/fields';
import {
TelemetrytypesFieldContextDTO,
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import cx from 'classnames';
import Spinner from 'components/Spinner';
import useDebounce from 'hooks/useDebounce';
import {
FieldContext,
FieldContextValue,
} from 'container/LLMObservability/AttributeMapping/types';
import styles from './KeySearchInput.module.scss';
const SUGGESTION_LIMIT = 50;
const DEBOUNCE_MS = 300;
interface KeySearchInputProps {
value: string;
fieldContext: FieldContextValue;
placeholder?: string;
className?: string;
disabled?: boolean;
testId?: string;
onChange: (value: string) => void;
}
// Maps the mapper's attribute/resource context to the fields-endpoint context.
function toFieldsContext(
context: FieldContextValue,
): TelemetrytypesFieldContextDTO {
return context === FieldContext.resource
? TelemetrytypesFieldContextDTO.resource
: TelemetrytypesFieldContextDTO.attribute;
}
// Free-text input with span/resource key suggestions from /api/v1/fields/keys
// (signal=traces). Typing keeps the custom value; suggestions are assistive.
function KeySearchInput({
value,
fieldContext,
placeholder,
className,
disabled,
testId,
onChange,
}: KeySearchInputProps): JSX.Element {
const [isOpen, setIsOpen] = useState(false);
const debouncedSearch = useDebounce(value, DEBOUNCE_MS);
const { data, isFetching } = useGetFieldsKeys(
{
signal: TelemetrytypesSignalDTO.traces,
fieldContext: toFieldsContext(fieldContext),
searchText: debouncedSearch,
limit: SUGGESTION_LIMIT,
},
{ query: { enabled: isOpen && !disabled, keepPreviousData: true } },
);
const suggestions = useMemo(() => {
const keys = data?.data?.keys ?? {};
return Object.keys(keys)
.filter((key) => key !== value)
.slice(0, SUGGESTION_LIMIT);
}, [data, value]);
return (
<div className={cx(styles.keySearch, className)}>
<Input
placeholder={placeholder}
value={value}
disabled={disabled}
autoComplete="off"
onChange={(event): void => {
onChange(event.target.value);
setIsOpen(true);
}}
onFocus={(): void => setIsOpen(true)}
onBlur={(): void => setIsOpen(false)}
testId={testId}
/>
{isOpen && suggestions.length > 0 && (
<div
className={styles.keySearchDropdown}
data-testid={`${testId}-dropdown`}
>
{suggestions.map((name) => (
<button
type="button"
key={name}
className={styles.keySearchOption}
title={name}
// onMouseDown (not onClick) so selection runs before the input blur.
onMouseDown={(event): void => {
event.preventDefault();
onChange(name);
setIsOpen(false);
}}
data-testid={`${testId}-option-${name}`}
>
{name}
</button>
))}
</div>
)}
{isOpen && isFetching && suggestions.length === 0 && (
<div
className={cx(styles.keySearchDropdown, styles.keySearchDropdownEmpty)}
>
<Spinner size="small" height="auto" />
</div>
)}
</div>
);
}
export default KeySearchInput;

View File

@@ -1,6 +1,7 @@
import { useState } from 'react';
import { Button } from '@signozhq/ui/button';
import { DrawerWrapper } from '@signozhq/ui/drawer';
import { Input } from '@signozhq/ui/input';
import { SelectSimple } from '@signozhq/ui/select';
import {
closestCenter,
@@ -19,7 +20,6 @@ import {
import { Plus, Trash2 } from '@signozhq/icons';
import { v4 as uuid } from 'uuid';
import KeySearchInput from '../KeySearchInput/KeySearchInput';
import styles from './MapperFormDrawer.module.scss';
import SourceAttributeRow from './components/SourceAttributeRow/SourceAttributeRow';
import {
@@ -164,12 +164,14 @@ function MapperFormDrawer({
<div className={styles.form}>
<div className={styles.field}>
<span className={styles.label}>Target attribute</span>
<KeySearchInput
<Input
placeholder="e.g. gen_ai.content.prompt"
value={draft.name}
fieldContext={draft.fieldContext}
disabled={isEdit}
onChange={(name): void => setDraft({ ...draft, name })}
autoComplete="off"
onChange={(event): void =>
setDraft({ ...draft, name: event.target.value })
}
testId="mapper-form-target"
/>
{isEdit && (

View File

@@ -1,4 +1,5 @@
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import { SelectSimple } from '@signozhq/ui/select';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
@@ -11,7 +12,6 @@ import {
MapperOperationValue,
SourceConfig,
} from 'container/LLMObservability/AttributeMapping/types';
import KeySearchInput from '../../../KeySearchInput/KeySearchInput';
import styles from './SourceAttributeRow.module.scss';
const CONTEXT_OPTIONS = [
@@ -66,12 +66,12 @@ function SourceAttributeRow({
<GripVertical size={14} />
</div>
<span className={styles.sourceIndex}>{index + 1}</span>
<KeySearchInput
<Input
className={styles.sourceInput}
placeholder="Source attribute key"
value={value.key}
fieldContext={value.context}
onChange={(key): void => onChange(index, { key })}
autoComplete="off"
onChange={(event): void => onChange(index, { key: event.target.value })}
testId={`mapper-form-source-${index}`}
/>
<SelectSimple

View File

@@ -123,7 +123,7 @@ export function buildUpdatableMapper(
export const EMPTY_GROUP_DRAFT: GroupDraft = {
id: null,
name: '',
attributes: [''],
attributes: [],
resource: [],
enabled: true,
};
@@ -170,8 +170,8 @@ export function buildDraftGroup(
localId: group.id,
serverId: group.id,
name: group.name,
attributes: group.condition?.attributes ?? [],
resource: group.condition?.resource ?? [],
attributes: cleanKeys(group.condition?.attributes ?? []),
resource: cleanKeys(group.condition?.resource ?? []),
enabled: group.enabled,
mappers: mappers.map(buildDraftMapper),
};
@@ -182,7 +182,7 @@ export function groupDraftFromNode(group: DraftGroup): GroupDraft {
return {
id: group.localId,
name: group.name,
attributes: group.attributes.length > 0 ? group.attributes : [''],
attributes: group.attributes,
resource: group.resource,
enabled: group.enabled,
};