Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions redisinsight/ui/src/constants/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ enum BrowserStorageItem {
vectorSearchQueryOnboarding = 'vectorSearchQueryOnboarding',
vectorSearchSelectKeyOnboarding = 'vectorSearchSelectKeyOnboarding',
vectorSearchCreateIndexOnboarding = 'vectorSearchCreateIndexOnboarding',
valueDecoderRules = 'valueDecoderRules',
wbTsResultPreferences = 'wbTsResultPreferences_',
prodModeCtaActioned = 'prodModeCtaActioned',
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React, { useCallback } from 'react'

import { PrimaryButton } from 'uiSrc/components/base/forms/buttons'
import { RiTooltip } from 'uiSrc/components'

import { useValueDecoder } from './ValueDecoderProvider'
import { VALUE_DECODER_TEST_ID } from './constants'

export const ConfigValueDecoderButton = () => {
const { openValueDecoderModal } = useValueDecoder()

const handleOpen = useCallback(() => {
openValueDecoderModal()
}, [openValueDecoderModal])

return (
<RiTooltip
position="top"
content="Configure shared decoder rules for matching hash key patterns."
>
<PrimaryButton
size="small"
onClick={handleOpen}
data-testid={`${VALUE_DECODER_TEST_ID}-config-btn`}
>
Value Decoders
</PrimaryButton>
</RiTooltip>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import styled from 'styled-components'

export const DecodedValueTooltipContent = styled.span`
display: inline-block;
width: max-content;
max-width: 90vw;
white-space: pre-wrap;
`
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React, { useMemo } from 'react'

import { Text } from 'uiSrc/components/base/text'
import { RedisResponseBuffer } from 'uiSrc/slices/interfaces'
import FormattedValue from 'uiSrc/pages/browser/modules/key-details/shared/formatted-value/FormattedValue'

import * as S from './DecodedValueDisplay.styles'
import { useValueDecoder } from './ValueDecoderProvider'
import {
formatParsedFields,
formatParsedFieldsInline,
parseBufferWithRule,
} from './utils'

export interface DecodedValueDisplayProps {
buffer: RedisResponseBuffer
fallback: React.ReactNode
expanded?: boolean
}

export const DecodedValueDisplay = ({
buffer,
fallback,
expanded,
}: DecodedValueDisplayProps) => {
const { matchedRule, isDecodeEnabled } = useValueDecoder()

const decodedNodes = useMemo(() => {
if (!isDecodeEnabled || !matchedRule) return null
return parseBufferWithRule(buffer, matchedRule.schema)
}, [buffer, isDecodeEnabled, matchedRule])

const formattedInline = useMemo(
() => (decodedNodes ? formatParsedFieldsInline(decodedNodes) : ''),
[decodedNodes],
)

const formattedMultiline = useMemo(
() => (decodedNodes ? formatParsedFields(decodedNodes) : ''),
[decodedNodes],
)

if (!decodedNodes) {
return <>{fallback}</>
}

if (decodedNodes.length === 0) {
return (
<Text color="secondary" component="span">
No decoded fields
</Text>
)
}

if (expanded) {
return (
<Text
color="secondary"
component="div"
style={{ maxWidth: '100%', whiteSpace: 'break-spaces' }}
>
{formattedMultiline}
</Text>
)
}

return (
<FormattedValue
value={formattedInline}
tooltipContent={
<S.DecodedValueTooltipContent>
{formattedMultiline}
</S.DecodedValueTooltipContent>
}
maxWidth="min(90vw, max-content)"
title="Value"
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import React, { useCallback } from 'react'

import { Text } from 'uiSrc/components/base/text'
import { ActionIconButton } from 'uiSrc/components/base/forms/buttons'
import { DeleteIcon } from 'uiSrc/components/base/icons'
import { Col } from 'uiSrc/components/base/layout/flex'
import { FormField } from 'uiSrc/components/base/forms/FormField'
import TextInput from 'uiSrc/components/base/inputs/TextInput'
import { RiSelect } from 'uiSrc/components/base/forms/select/RiSelect'

import {
DECODER_TYPE_OPTIONS,
VALUE_DECODER_TEST_ID,
} from './constants'
import {
DECODER_TYPE_DESCRIPTIONS,
KEY_PATTERN_FIELD_DESCRIPTION,
} from './descriptions'
import { createDescriptionSelectValueRender } from './DescriptionSelectValueRender'
import { FieldsSchemaEditor } from './FieldsSchemaEditor'
import { KeyPatternsEditor } from './KeyPatternsEditor'
import { isDecoderValid } from './schemaUtils'
import { DecoderType, SchemaNode, ValueDecoderRule } from './types'
import * as S from './ValueDecoderModal.styles'

const decoderTypeOptions = DECODER_TYPE_OPTIONS.map((option) => ({
value: option.value,
label: option.content,
}))

const decoderTypeValueRender = createDescriptionSelectValueRender(
DECODER_TYPE_DESCRIPTIONS,
)

export interface DecoderEditorProps {
decoder: ValueDecoderRule
isExpanded: boolean
onToggle: () => void
onChange: (decoder: ValueDecoderRule) => void
onRemove: () => void
canRemove: boolean
summary: string
matchesCurrentKey?: boolean
}

export const DecoderEditor = ({
decoder,
isExpanded,
onToggle,
onChange,
onRemove,
canRemove,
summary,
matchesCurrentKey,
}: DecoderEditorProps) => {
const handleFieldChange = useCallback(
<K extends keyof ValueDecoderRule>(key: K, value: ValueDecoderRule[K]) => {
onChange({ ...decoder, [key]: value })
},
[decoder, onChange],
)

const isValid = isDecoderValid(decoder)

return (
<S.DecoderSection $expanded={isExpanded} data-testid={`${VALUE_DECODER_TEST_ID}-decoder-${decoder.id}`}>
<S.DecoderHeader>
<S.DecoderSummaryButton
type="button"
onClick={onToggle}
data-testid={`${VALUE_DECODER_TEST_ID}-decoder-toggle-${decoder.id}`}
>
<Text variant="semiBold" component="span">
{summary}
</Text>
{matchesCurrentKey && (
<S.DecoderMatchBadge>Matches current key</S.DecoderMatchBadge>
)}
{!isValid && (
<S.DecoderMatchBadge $warning>Incomplete</S.DecoderMatchBadge>
)}
</S.DecoderSummaryButton>
<ActionIconButton
icon={DeleteIcon}
aria-label="Remove decoder"
onClick={onRemove}
disabled={!canRemove}
data-testid={`${VALUE_DECODER_TEST_ID}-remove-decoder-${decoder.id}`}
/>
</S.DecoderHeader>

{isExpanded && (
<S.DecoderBody>
<FormField label="Name" additionalText="Optional label for this decoder.">
<TextInput
value={decoder.name}
onChange={(value) => handleFieldChange('name', value)}
placeholder="Chunk state decoder"
data-testid={`${VALUE_DECODER_TEST_ID}-decoder-name-${decoder.id}`}
/>
</FormField>

<FormField
label="Key Patterns"
required
additionalText={KEY_PATTERN_FIELD_DESCRIPTION}
>
<KeyPatternsEditor
listId={`decoder-${decoder.id}-patterns`}
patterns={
decoder.keyPatterns.length > 0 ? decoder.keyPatterns : ['']
}
onChange={(keyPatterns) => handleFieldChange('keyPatterns', keyPatterns)}
/>
</FormField>

<FormField label="Decoder Type" required>
<RiSelect
options={decoderTypeOptions}
value={decoder.decoderType}
onChange={(value) =>
handleFieldChange('decoderType', value as DecoderType)
}
valueRender={decoderTypeValueRender}
data-testid={`${VALUE_DECODER_TEST_ID}-decoder-type-${decoder.id}`}
/>
</FormField>

<Col gap="s">
<Text variant="semiBold">Fields</Text>
<FieldsSchemaEditor
sortListId={`decoder-${decoder.id}-schema`}
nodes={decoder.schema}
onChange={(schema: SchemaNode[]) => handleFieldChange('schema', schema)}
/>
</Col>
</S.DecoderBody>
)}
</S.DecoderSection>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { RiTooltip } from 'uiSrc/components'

import {
SelectValueRender,
SelectValueRenderParams,
} from 'uiSrc/components/base/forms/select/RiSelect'

import * as S from './ValueDecoderModal.styles'

export const createDescriptionSelectValueRender = (
descriptions: Record<string, string>,
): SelectValueRender => {
const render = ({ option, isOptionValue }: SelectValueRenderParams) => {
const description = descriptions[String(option.value)] ?? ''
const label = option.label ?? option.value

return (
<RiTooltip
content={description}
position={isOptionValue ? 'right' : 'top'}
delay={150}
>
<S.SelectOptionAnchor $fullWidth={isOptionValue}>
{label}
</S.SelectOptionAnchor>
</RiTooltip>
)
}

return render
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react'

type EyeIconProps = {
size?: number
className?: string
}

export const EyeIcon = ({ size = 16, className }: EyeIconProps) => (
<svg
width={size}
height={size}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
aria-hidden
>
<path
d="M8 3C4.5 3 1.73 5.11 1 8c.73 2.89 3.5 5 7 5s6.27-2.11 7-5c-.73-2.89-3.5-5-7-5zm0 8.5A3.5 3.5 0 1 1 8 4.5a3.5 3.5 0 0 1 0 7z"
fill="currentColor"
/>
<circle cx="8" cy="8" r="1.75" fill="currentColor" />
</svg>
)

export const EyeOffIcon = ({ size = 16, className }: EyeIconProps) => (
<svg
width={size}
height={size}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
aria-hidden
>
<path
d="M2.15 1.15a.5.5 0 0 0-.7.7l12 12a.5.5 0 0 0 .7-.7l-2.2-2.2A11.7 11.7 0 0 0 15 8c-.73-2.89-3.5-5-7-5a6.9 6.9 0 0 0-3.9 1.2L2.15 1.15zM8 11.5A3.5 3.5 0 0 1 4.5 8c0-.7.2-1.35.55-1.9L9.9 10.95A3.46 3.46 0 0 1 8 11.5zM1 8c.35 1.38 1.2 2.58 2.35 3.45l1.05-1.05A5.96 5.96 0 0 1 2.5 8c.18-.62.45-1.2.8-1.72L2.25 5.23A6.96 6.96 0 0 0 1 8z"
fill="currentColor"
/>
</svg>
)
Loading