Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
6 changes: 5 additions & 1 deletion redisinsight/api/config/features-config.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": 3.94,
"version": 3.95,
"features": {
"redisDataIntegration": {
"flag": true,
Expand Down Expand Up @@ -150,6 +150,10 @@
"prodMode": {
"flag": true,
"perc": [[0, 100]]
},
"valueDecoder": {
"flag": false,
"perc": [[0, 100]]
}
}
}
1 change: 1 addition & 0 deletions redisinsight/api/src/modules/feature/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export enum KnownFeatures {
VectorSet = 'vectorSet',
DevArray = 'dev-array',
ProdMode = 'prodMode',
ValueDecoder = 'valueDecoder',
}

export interface IFeatureFlag {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,8 @@ export const knownFeatures: Record<KnownFeatures, IFeatureFlag> = {
name: KnownFeatures.ProdMode,
storage: FeatureStorage.Database,
},
[KnownFeatures.ValueDecoder]: {
name: KnownFeatures.ValueDecoder,
storage: FeatureStorage.Database,
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ export class FeatureFlagProvider {
KnownFeatures.ProdMode,
new CommonFlagStrategy(this.featuresConfigService, this.settingsService),
);
this.strategies.set(
KnownFeatures.ValueDecoder,
new CommonFlagStrategy(this.featuresConfigService, this.settingsService),
);
}

getStrategy(name: string): FeatureFlagStrategy {
Expand Down
1 change: 1 addition & 0 deletions redisinsight/ui/src/constants/featureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ export enum FeatureFlags {
azureEntraId = 'azureEntraId',
devBrowser = 'dev-browser',
prodMode = 'prodMode',
valueDecoder = 'valueDecoder',
}
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 { EmptyButton } 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."
>
<EmptyButton
size="small"
onClick={handleOpen}
data-testid={`${VALUE_DECODER_TEST_ID}-config-btn`}
>
Value Decoders
</EmptyButton>
</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,151 @@
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 { CopyButton } from 'uiSrc/components/copy-button/CopyButton'

import {
DECODER_TYPE_OPTIONS,
VALUE_DECODER_TEST_ID,
} from './constants'
import { serializeDecoderForClipboard } from './decoderClipboard'
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>
<S.RowActions>
<CopyButton
copy={serializeDecoderForClipboard(decoder)}
aria-label="Copy decoder"
tooltipConfig={{ content: 'Copy decoder' }}
data-testid={`${VALUE_DECODER_TEST_ID}-copy-decoder-${decoder.id}`}
/>
<ActionIconButton
icon={DeleteIcon}
aria-label="Remove decoder"
onClick={onRemove}
disabled={!canRemove}
data-testid={`${VALUE_DECODER_TEST_ID}-remove-decoder-${decoder.id}`}
/>
</S.RowActions>
</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
}
Loading