Skip to content
Open
Show file tree
Hide file tree
Changes from 13 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": 5,
"version": 5.1,
"features": {
"dev-language": {
"flag": true,
Expand Down Expand Up @@ -155,6 +155,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',
DevLanguage = 'dev-language',
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ export const knownFeatures: Record<KnownFeatures, IFeatureFlag> = {
name: KnownFeatures.ProdMode,
storage: FeatureStorage.Database,
},
[KnownFeatures.ValueDecoder]: {
name: KnownFeatures.ValueDecoder,
storage: FeatureStorage.Database,
},
[KnownFeatures.DevLanguage]: {
name: KnownFeatures.DevLanguage,
storage: FeatureStorage.Database,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ export class FeatureFlagProvider {
KnownFeatures.ProdMode,
new CommonFlagStrategy(this.featuresConfigService, this.settingsService),
);
this.strategies.set(
KnownFeatures.ValueDecoder,
new CommonFlagStrategy(this.featuresConfigService, this.settingsService),
);
this.strategies.set(
KnownFeatures.DevLanguage,
new SwitchableFlagStrategy(
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,5 +17,6 @@ export enum FeatureFlags {
azureEntraId = 'azureEntraId',
devBrowser = 'dev-browser',
prodMode = 'prodMode',
valueDecoder = 'valueDecoder',
devLanguage = 'dev-language',
}
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