mirror of
https://github.com/SigNoz/signoz.git
synced 2026-08-17 10:20:31 +01:00
Compare commits
2 Commits
main
...
feat/ai-o1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09884cf7d6 | ||
|
|
8d0eda4a0d |
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}`}
|
||||
>
|
||||
|
||||
@@ -44,7 +44,7 @@ function MapperActionsMenu({
|
||||
aria-label="Mapping actions"
|
||||
testId={`mapper-actions-${mapper.localId}`}
|
||||
>
|
||||
<EllipsisVertical size={16} />
|
||||
<EllipsisVertical size={14} />
|
||||
</Button>
|
||||
</DropdownMenuSimple>
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}`}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -58,3 +58,7 @@
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.drawerLabel {
|
||||
composes: drawerLabel from './shared.module.scss';
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
*
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user