mirror of
https://github.com/SigNoz/signoz.git
synced 2026-08-17 10:20:31 +01:00
Compare commits
1 Commits
main
...
feat/ai-o1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d0eda4a0d |
@@ -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);
|
||||
|
||||
@@ -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 })}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user