Compare commits

...

2 Commits

Author SHA1 Message Date
Gaurav Tewari
09884cf7d6 feat: ui changes in ai o11y 2026-08-17 12:28:03 +05:30
Gaurav Tewari
8d0eda4a0d feat: new ui update 2026-08-17 10:56:46 +05:30
32 changed files with 385 additions and 388 deletions

View File

@@ -446,7 +446,7 @@ describe('AttributeMappingsTab (integration)', () => {
await expect(
screen.findByTestId('mapper-form-drawer'),
).resolves.toBeInTheDocument();
expect(screen.getByText('New custom mapping')).toBeInTheDocument();
expect(screen.getByText('New Custom Mapping')).toBeInTheDocument();
});
it('opens the edit drawer prefilled and locks the target attribute', async () => {
@@ -459,7 +459,7 @@ describe('AttributeMappingsTab (integration)', () => {
await expect(
screen.findByTestId('mapper-form-drawer'),
).resolves.toBeInTheDocument();
expect(screen.getByText('Edit mapping')).toBeInTheDocument();
expect(screen.getByText('Edit Mapping')).toBeInTheDocument();
const target = screen.getByTestId('mapper-form-target');
expect(target).toHaveValue('gen_ai.request.model');
expect(target).toBeDisabled();

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

@@ -1,7 +1,10 @@
.pageHeader {
display: flex;
align-items: flex-start;
align-items: center;
justify-content: space-between;
// Holds the row at button height so mounting the unsaved-changes actions
// doesn't push the page down.
min-height: 2rem;
margin-left: var(--spacing-2);
margin-top: var(--spacing-4);
}

View File

@@ -0,0 +1,19 @@
.label {
display: inline-flex;
align-items: center;
gap: var(--spacing-2);
}
.labelInfo {
display: inline-flex;
align-items: center;
padding: 0;
background: transparent;
border: none;
color: var(--l2-foreground);
cursor: pointer;
&:hover {
color: var(--l1-foreground);
}
}

View File

@@ -0,0 +1,43 @@
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Info } from '@signozhq/icons';
import cx from 'classnames';
import styles from './FieldLabel.module.scss';
interface FieldLabelProps {
label: string;
hint?: string;
className?: string;
testId?: string;
}
// Field label with its explanation tucked behind an info icon. Callers supply
// their own typography via className; this owns the icon and its alignment.
function FieldLabel({
label,
hint,
className,
testId,
}: FieldLabelProps): JSX.Element {
return (
<span className={cx(styles.label, className)}>
{label}
{hint && (
// withPortal={false} — the drawer traps focus, so a portalled tooltip
// would render outside it.
<TooltipSimple title={hint} side="top" align="start" withPortal={false}>
<button
type="button"
className={styles.labelInfo}
aria-label={hint}
data-testid={testId}
>
<Info size={12} />
</button>
</TooltipSimple>
)}
</span>
);
}
export default FieldLabel;

View File

@@ -1,7 +1,7 @@
.groupForm {
display: flex;
flex-direction: column;
gap: var(--spacing-10);
gap: var(--spacing-16);
padding: var(--spacing-2) 0;
}
@@ -18,16 +18,26 @@
}
.groupFormLabel {
font-size: var(--font-size-xs);
font-weight: var(--font-weight-semibold);
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--l3-foreground);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
color: var(--l1-foreground);
}
.groupFormHint {
font-size: var(--font-size-xs);
color: var(--l3-foreground);
color: var(--l2-foreground);
}
.groupFormSection {
display: flex;
flex-direction: column;
gap: var(--spacing-10);
}
.groupFormSectionHead {
display: flex;
flex-direction: column;
gap: var(--spacing-2);
}
.groupFormFooter {

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 {
@@ -36,7 +36,7 @@ function GroupFormDrawer({
onClose();
}
}}
title={isEdit ? 'Edit group' : 'New group'}
title={isEdit ? 'Edit Group' : 'New Group'}
subTitle="A group gates which spans its mappings run on"
width="wide"
testId="group-form-drawer"
@@ -57,58 +57,64 @@ function GroupFormDrawer({
disabled={!isValid}
testId="group-form-save"
>
{isEdit ? 'Save group' : 'Create group'}
{isEdit ? 'Save Group' : 'Create Group'}
</Button>
</div>
}
>
<div className={styles.groupForm}>
<div className={styles.groupFormField}>
<span className={styles.groupFormLabel}>Group Name</span>
<Input
placeholder="e.g. OpenAI gateway"
value={draft.name}
onChange={(event): void =>
setDraft({ ...draft, name: event.target.value })
}
testId="group-form-name"
<section className={styles.groupFormSection}>
<div className={styles.groupFormField}>
<span className={styles.groupFormLabel}>Group Name</span>
<Input
placeholder="e.g. OpenAI Gateway"
value={draft.name}
onChange={(event): void =>
setDraft({ ...draft, name: event.target.value })
}
testId="group-form-name"
/>
</div>
<div className={`${styles.groupFormField} ${styles.groupFormFieldRow}`}>
<span className={styles.groupFormLabel}>Enabled</span>
<Switch
value={draft.enabled}
onChange={(checked): void => setDraft({ ...draft, enabled: checked })}
testId="group-form-enabled"
/>
</div>
</section>
<section className={styles.groupFormSection}>
<div className={styles.groupFormSectionHead}>
<span className={styles.groupFormLabel}>Conditions</span>
<span className={styles.groupFormHint}>
Runs when either list matches. Leave both empty to run this group on
every span.
</span>
</div>
<ConditionKeyList
label="Span Attribute Keys"
labelHint="Runs when a span attribute key contains any of these"
keys={draft.attributes}
placeholder="e.g. gen_ai."
addLabel="Add attribute key"
testIdPrefix="group-form-attribute"
onChange={(attributes): void => setDraft({ ...draft, attributes })}
/>
</div>
<div className={`${styles.groupFormField} ${styles.groupFormFieldRow}`}>
<span className={styles.groupFormLabel}>Enabled</span>
<Switch
value={draft.enabled}
onChange={(checked): void => setDraft({ ...draft, enabled: checked })}
testId="group-form-enabled"
<ConditionKeyList
label="Resource Keys"
labelHint="Runs when a resource key contains any of these"
keys={draft.resource}
placeholder="e.g. service.name"
addLabel="Add resource key"
testIdPrefix="group-form-resource"
onChange={(resource): void => setDraft({ ...draft, resource })}
/>
</div>
<ConditionKeyList
label="Condition · span attribute keys"
labelHint="·Runs when a span attribute key contains any of these"
keys={draft.attributes}
placeholder="e.g. gen_ai."
addLabel="Add attribute key"
testIdPrefix="group-form-attribute"
fieldContext={FieldContext.attribute}
onChange={(attributes): void => setDraft({ ...draft, attributes })}
/>
<ConditionKeyList
label="Condition · resource keys"
labelHint="·Or when a resource key contains any of these"
keys={draft.resource}
placeholder="e.g. service.name"
addLabel="Add resource key"
testIdPrefix="group-form-resource"
fieldContext={FieldContext.resource}
onChange={(resource): void => setDraft({ ...draft, resource })}
/>
<span className={styles.groupFormHint}>
Leave both empty to run this group on every span.
</span>
</section>
</div>
</DrawerWrapper>
);

View File

@@ -6,27 +6,53 @@
.label {
font-size: var(--font-size-xs);
font-weight: var(--font-weight-semibold);
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--l3-foreground);
font-weight: var(--font-weight-medium);
color: var(--l1-foreground);
}
.labelHint {
font-weight: var(--font-weight-normal);
text-transform: none;
letter-spacing: normal;
}
.keys {
.keyBox {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
}
.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,10 @@
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 FieldLabel from '../../../FieldLabel/FieldLabel';
import styles from './ConditionKeyList.module.scss';
interface ConditionKeyListProps {
@@ -12,10 +14,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,65 +26,87 @@ 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 (
<div className={styles.field}>
<span className={styles.label}>
{label}
{labelHint && <span className={styles.labelHint}> {labelHint}</span>}
</span>
<FieldLabel
label={label}
hint={labelHint}
className={styles.label}
testId={`${testIdPrefix}-hint`}
/>
{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

@@ -12,22 +12,14 @@
}
.label {
font-size: var(--font-size-xs);
font-weight: var(--font-weight-semibold);
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--l3-foreground);
}
.labelHint {
font-weight: var(--font-weight-normal);
text-transform: none;
letter-spacing: normal;
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
color: var(--l1-foreground);
}
.hint {
font-size: var(--font-size-xs);
color: var(--l3-foreground);
color: var(--l2-foreground);
}
.fieldContext {

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,7 @@ import {
import { Plus, Trash2 } from '@signozhq/icons';
import { v4 as uuid } from 'uuid';
import KeySearchInput from '../KeySearchInput/KeySearchInput';
import FieldLabel from '../FieldLabel/FieldLabel';
import styles from './MapperFormDrawer.module.scss';
import SourceAttributeRow from './components/SourceAttributeRow/SourceAttributeRow';
import {
@@ -120,8 +121,8 @@ function MapperFormDrawer({
onClose();
}
}}
title={isEdit ? 'Edit mapping' : 'New custom mapping'}
subTitle="Map source attributes onto a canonical target attribute"
title={isEdit ? 'Edit Mapping' : 'New Custom Mapping'}
subTitle="Map Source Attributes Onto a Canonical Target Attribute"
width="wide"
testId="mapper-form-drawer"
footer={
@@ -163,13 +164,15 @@ function MapperFormDrawer({
>
<div className={styles.form}>
<div className={styles.field}>
<span className={styles.label}>Target attribute</span>
<KeySearchInput
<span className={styles.label}>Target Attribute</span>
<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 && (
@@ -180,7 +183,12 @@ function MapperFormDrawer({
</div>
<div className={styles.field}>
<span className={styles.label}>Write target to</span>
<FieldLabel
label="Write Target To"
hint="Where the standardized attribute is written."
className={styles.label}
testId="mapper-form-field-context-hint"
/>
<SelectSimple
className={styles.fieldContext}
items={FIELD_CONTEXT_OPTIONS}
@@ -191,19 +199,15 @@ function MapperFormDrawer({
}
testId="mapper-form-field-context"
/>
<span className={styles.hint}>
Where the standardized attribute is written.
</span>
</div>
<div className={styles.field}>
<span className={styles.label}>
Source attributes
<span className={styles.labelHint}>
{' '}
· priority: top bottom · drag to reorder
</span>
</span>
<FieldLabel
label="Source Attributes"
hint="Sources are tried top to bottom - drag to reorder."
className={styles.label}
testId="mapper-form-sources-hint"
/>
<DndContext
sensors={sensors}
@@ -235,7 +239,7 @@ function MapperFormDrawer({
onClick={addSource}
testId="mapper-form-add-source"
>
Add another source
Add Another Source
</Button>
</div>

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,
};

View File

@@ -58,3 +58,7 @@
padding: 0;
width: 100%;
}
.drawerLabel {
composes: drawerLabel from './shared.module.scss';
}

View File

@@ -109,7 +109,7 @@ function ModelCostDrawer({
drawerHeaderProps={{ className: styles.title }}
>
<div className={styles.drawerSection}>
<label htmlFor="billing-model-id">
<label htmlFor="billing-model-id" className={styles.drawerLabel}>
Billing Model ID{' '}
<span className={styles.required} aria-hidden="true">
*
@@ -144,7 +144,9 @@ function ModelCostDrawer({
</div>
<div className={styles.drawerSection}>
<label htmlFor="provider-select">Provider</label>
<label htmlFor="provider-select" className={styles.drawerLabel}>
Provider
</label>
<Controller
name="provider"
control={control}

View File

@@ -49,16 +49,11 @@
flex-direction: column;
align-items: flex-start;
gap: var(--spacing-5);
padding: var(--spacing-6);
border-radius: 6px;
border: 1px solid var(--l2-border);
}
.bucketPickerTitle {
font-size: var(--periscope-font-size-small);
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--l3-foreground);
color: var(--l2-foreground);
}
.bucketPickerChips {
@@ -66,3 +61,7 @@
flex-wrap: wrap;
gap: var(--spacing-4);
}
.drawerSubLabel {
composes: drawerSubLabel from '../../shared.module.scss';
}

View File

@@ -67,9 +67,7 @@ function ExtraPricingBuckets({
return (
<div className={cx(styles.extraBucketsSection, styles.drawerSection)}>
<div className={styles.extraBucketsSectionHead}>
<Typography.Text as="span" size="small" color="muted">
Extra Pricing Buckets
</Typography.Text>
<span className={styles.drawerSubLabel}>Extra Pricing Buckets</span>
<Typography.Text as="span" size="small" color="muted">
Optional
</Typography.Text>
@@ -116,7 +114,9 @@ function ExtraPricingBuckets({
{addedBuckets.length > 0 && (
<div className={cx(styles.pricingField, styles.cacheModeField)}>
<label htmlFor="cache-mode">Cache mode</label>
<label htmlFor="cache-mode" className={styles.drawerSubLabel}>
Cache mode
</label>
<SelectSimple
id="cache-mode"
value={pricing.cacheMode}

View File

@@ -9,9 +9,7 @@
.patternBox {
display: flex;
flex-direction: column;
padding: var(--spacing-6);
border-radius: 6px;
border: 1px solid var(--l2-border);
gap: var(--spacing-4);
}
.patternChips {
@@ -46,3 +44,7 @@
display: flex;
gap: var(--spacing-3);
}
.drawerLabel {
composes: drawerLabel from '../../shared.module.scss';
}

View File

@@ -37,12 +37,12 @@ function PatternEditor({
return (
<div className={styles.drawerSection}>
<Typography.Text as="span">
Model name patterns{' '}
<span className={styles.drawerLabel}>
Model Name Patterns{' '}
<Typography.Text as="span" color="muted">
(prefix match)
(Prefix Match)
</Typography.Text>
</Typography.Text>
</span>
<div className={styles.patternBox}>
<div className={styles.patternChips}>
{patterns.map((pattern) => (

View File

@@ -2,12 +2,12 @@
composes: drawerSection from '../../shared.module.scss';
}
.drawerSurface {
composes: drawerSurface from '../../shared.module.scss';
.drawerFieldset {
composes: drawerFieldset from '../../shared.module.scss';
}
.drawerSurfaceHead {
composes: drawerSurfaceHead from '../../shared.module.scss';
.drawerFieldsetHead {
composes: drawerFieldsetHead from '../../shared.module.scss';
}
.managedLabel {
@@ -29,3 +29,11 @@
grid-template-columns: 1fr 1fr;
gap: var(--spacing-6);
}
.drawerLabel {
composes: drawerLabel from '../../shared.module.scss';
}
.drawerSubLabel {
composes: drawerSubLabel from '../../shared.module.scss';
}

View File

@@ -22,11 +22,9 @@ function PricingFields({
onChange,
}: PricingFieldsProps): JSX.Element {
return (
<div className={cx(styles.drawerSection, styles.drawerSurface)}>
<div className={styles.drawerSurfaceHead}>
<Typography.Text size="base" weight="bold">
Pricing (per 1M tokens, USD)
</Typography.Text>
<div className={cx(styles.drawerSection, styles.drawerFieldset)}>
<div className={styles.drawerFieldsetHead}>
<span className={styles.drawerLabel}>Pricing (per 1M tokens, USD)</span>
{isReadOnly && (
<span className={styles.managedLabel} data-testid="drawer-readonly-label">
@@ -38,7 +36,7 @@ function PricingFields({
</div>
<div className={styles.pricingGrid}>
<div className={styles.pricingField}>
<label htmlFor="input-cost">
<label htmlFor="input-cost" className={styles.drawerSubLabel}>
Input Cost{' '}
<span className={styles.required} aria-hidden="true">
*
@@ -58,7 +56,7 @@ function PricingFields({
/>
</div>
<div className={styles.pricingField}>
<label htmlFor="output-cost">
<label htmlFor="output-cost" className={styles.drawerSubLabel}>
Output Cost{' '}
<span className={styles.required} aria-hidden="true">
*

View File

@@ -2,12 +2,12 @@
composes: drawerSection from '../../shared.module.scss';
}
.drawerSurface {
composes: drawerSurface from '../../shared.module.scss';
.drawerFieldset {
composes: drawerFieldset from '../../shared.module.scss';
}
.drawerSurfaceHead {
composes: drawerSurfaceHead from '../../shared.module.scss';
.drawerFieldsetHead {
composes: drawerFieldsetHead from '../../shared.module.scss';
}
.managedLabel {
@@ -86,7 +86,7 @@
.sourceRadioDesc {
margin-top: 2px;
font-size: 12px;
color: var(--l3-foreground);
color: var(--l2-foreground);
}
.resetConfirm {
@@ -107,3 +107,7 @@
gap: var(--spacing-4);
justify-content: flex-end;
}
.drawerLabel {
composes: drawerLabel from '../../shared.module.scss';
}

View File

@@ -2,7 +2,6 @@ import { useState } from 'react';
import { Button } from '@signozhq/ui/button';
import { RadioGroup, RadioGroupItem } from '@signozhq/ui/radio-group';
import { Lock } from '@signozhq/icons';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import styles from './SourceSelector.module.scss';
@@ -40,11 +39,9 @@ function SourceSelector({
};
return (
<div className={cx(styles.drawerSection, styles.drawerSurface)}>
<div className={styles.drawerSurfaceHead}>
<Typography.Text weight="bold" size="base">
Source
</Typography.Text>
<div className={cx(styles.drawerSection, styles.drawerFieldset)}>
<div className={styles.drawerFieldsetHead}>
<span className={styles.drawerLabel}>Source</span>
{isReadOnly && (
<span className={styles.managedLabel} data-testid="drawer-managed-label">

View File

@@ -22,17 +22,26 @@
font-size: 10px;
}
.drawerSurface {
padding: var(--spacing-7);
border-radius: 6px;
border: 1px solid var(--l2-border);
.drawerLabel {
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
color: var(--l1-foreground);
}
.drawerSurfaceHead {
.drawerSubLabel {
font-size: var(--font-size-xs);
font-weight: var(--font-weight-medium);
color: var(--l1-foreground);
}
.drawerFieldset {
gap: var(--spacing-5);
}
.drawerFieldsetHead {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--spacing-5);
}
.managedLabel {
@@ -40,7 +49,7 @@
align-items: center;
gap: var(--spacing-2);
font-size: var(--periscope-font-size-small);
color: var(--l3-foreground);
color: var(--l2-foreground);
}
.required {

View File

@@ -58,8 +58,8 @@ export const CACHE_MODE_OPTIONS = [
];
export const CACHE_BUCKETS: CacheBucketDef[] = [
{ key: 'cacheRead', label: 'cache_read', testId: 'cache-read' },
{ key: 'cacheWrite', label: 'cache_write', testId: 'cache-write' },
{ key: 'cacheRead', label: 'Cache Read', testId: 'cache-read' },
{ key: 'cacheWrite', label: 'Cache Write', testId: 'cache-write' },
];
export const EMPTY_DRAFT: DrawerDraft = {