Skip to content

Commit

Permalink
refactor(18161): Change the persistence of the metrics to a local sto…
Browse files Browse the repository at this point in the history
…re (#243)

* feat(18161): add a store for the metrics

* refactor(18161): replace the deprecated localStorage with the new met…

* test(18161): add tests

* chore(18077): little bit of cleaning
  • Loading branch information
vanch3d committed Dec 19, 2023
1 parent 8b6ce20 commit 3fc2f3c
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 20 deletions.
47 changes: 28 additions & 19 deletions hivemq-edge/src/frontend/src/modules/Metrics/Metrics.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FC } from 'react'
import { FC, useEffect } from 'react'
import {
Accordion,
AccordionButton,
Expand All @@ -12,13 +12,13 @@ import {
useDisclosure,
} from '@chakra-ui/react'
import { useTranslation } from 'react-i18next'
import { useLocalStorage } from '@uidotdev/usehooks'

import { NodeTypes } from '@/modules/Workspace/types.ts'

import config from '@/config'

import { ChartType, MetricDefinition } from './types.ts'
import useMetricsStore from './hooks/useMetricsStore.ts'
import MetricEditor from './components/editor/MetricEditor.tsx'
import ChartContainer from './components/container/ChartContainer.tsx'
import Sample from './components/container/Sample.tsx'
Expand All @@ -37,23 +37,32 @@ export interface MetricSpecStorage {
}

const Metrics: FC<MetricsProps> = ({ nodeId, adapterIDs, initMetrics, defaultChartType }) => {
const [metrics, setMetrics] = useLocalStorage<MetricSpecStorage[]>(
`edge.reports-${nodeId}`,
initMetrics ? initMetrics.map<MetricSpecStorage>((e) => ({ selectedTopic: e })) : []
)
const showEditor = config.features.METRICS_SHOW_EDITOR
const { isOpen, onOpen, onClose } = useDisclosure()
const { t } = useTranslation()
const { isOpen, onOpen, onClose } = useDisclosure()
const { addMetrics, getMetricsFor, removeMetrics } = useMetricsStore()
const showEditor = config.features.METRICS_SHOW_EDITOR

const handleCreateMetrics = (value: MetricDefinition) => {
const { selectedTopic, selectedChart } = value
setMetrics((old) => [...old, { selectedTopic: selectedTopic?.value, selectedChart: selectedChart?.value }])
addMetrics(nodeId, selectedTopic.value, selectedChart?.value)
}

const handleRemoveMetrics = (selectedTopic: string) => {
setMetrics((old) => old.filter((x) => x.selectedTopic !== selectedTopic))
removeMetrics(nodeId, selectedTopic)
}

const metrics = getMetricsFor(nodeId)

useEffect(() => {
const gg = getMetricsFor(nodeId)
if (gg.length === 0 && initMetrics) {
initMetrics.map((e) => {
addMetrics(nodeId, e, defaultChartType)
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

return (
<Card size={'sm'}>
{showEditor && (
Expand All @@ -75,7 +84,7 @@ const Metrics: FC<MetricsProps> = ({ nodeId, adapterIDs, initMetrics, defaultCha
<AccordionPanel pb={4}>
<MetricEditor
filter={adapterIDs}
selectedMetrics={metrics.map((e) => e.selectedTopic)}
selectedMetrics={metrics.map((e) => e.metrics)}
selectedChart={defaultChartType}
onSubmit={handleCreateMetrics}
/>
Expand All @@ -87,22 +96,22 @@ const Metrics: FC<MetricsProps> = ({ nodeId, adapterIDs, initMetrics, defaultCha
<CardBody>
<SimpleGrid spacing={4} templateColumns="repeat(auto-fill, minmax(200px, 1fr))">
{metrics.map((e) => {
if (!e.selectedChart || e.selectedChart === ChartType.SAMPLE)
if (!e.chart || e.chart === ChartType.SAMPLE)
return (
<Sample
key={e.selectedTopic}
metricName={e.selectedTopic}
onClose={() => handleRemoveMetrics(e.selectedTopic)}
key={e.metrics}
metricName={e.metrics}
onClose={() => handleRemoveMetrics(e.metrics)}
canEdit={isOpen}
/>
)
else
return (
<ChartContainer
key={e.selectedTopic}
chartType={e.selectedChart}
metricName={e.selectedTopic}
onClose={() => handleRemoveMetrics(e.selectedTopic)}
key={e.metrics}
chartType={e.chart}
metricName={e.metrics}
onClose={() => handleRemoveMetrics(e.metrics)}
canEdit={isOpen}
/>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { expect } from 'vitest'
import { act, renderHook } from '@testing-library/react'

import useMetricsStore, {
MetricDefinitionSpec,
MetricDefinitionStore,
} from '@/modules/Metrics/hooks/useMetricsStore.ts'
import { ChartType } from '@/modules/Metrics/types.ts'

describe('useMetricsStore', () => {
beforeEach(() => {
const { result } = renderHook<MetricDefinitionStore, unknown>(useMetricsStore)
act(() => {
result.current.reset()
})
})

it('should start with an empty store', async () => {
const { result } = renderHook<MetricDefinitionStore, unknown>(useMetricsStore)
const { metrics } = result.current

expect(metrics).toHaveLength(0)
})

it('should add a metric to the store', async () => {
const { result } = renderHook<MetricDefinitionStore, unknown>(useMetricsStore)
const { metrics } = result.current

expect(metrics).toHaveLength(0)

act(() => {
const { addMetrics } = result.current
addMetrics('nodeId', 'metricNAme', ChartType.SAMPLE)
})

expect(result.current.metrics).toHaveLength(1)
})

it('should remove a metric to the store', async () => {
const { result } = renderHook<MetricDefinitionStore, unknown>(useMetricsStore)
const { metrics } = result.current

expect(metrics).toHaveLength(0)

act(() => {
const { addMetrics } = result.current
addMetrics('nodeId', 'metricName1', ChartType.SAMPLE)
addMetrics('nodeId', 'metricName2', ChartType.SAMPLE)
addMetrics('anotherNodeId', 'metricName1', ChartType.SAMPLE)
})

expect(result.current.metrics).toHaveLength(3)

act(() => {
const { removeMetrics } = result.current
removeMetrics('nodeId', 'metricName1')
})

expect(result.current.metrics).toStrictEqual<MetricDefinitionSpec[]>([
{
chart: ChartType.SAMPLE,
metrics: 'metricName2',
source: 'nodeId',
},
{
chart: ChartType.SAMPLE,
metrics: 'metricName1',
source: 'anotherNodeId',
},
])
})

it('should return all the metrics for a node', async () => {
const { result } = renderHook<MetricDefinitionStore, unknown>(useMetricsStore)
const { metrics } = result.current

expect(metrics).toHaveLength(0)

act(() => {
const { addMetrics } = result.current
addMetrics('nodeId', 'metricName1', ChartType.SAMPLE)
addMetrics('nodeId', 'metricName2', ChartType.SAMPLE)
addMetrics('anotherNodeId', 'metricName1', ChartType.SAMPLE)
})

expect(result.current.metrics).toHaveLength(3)

act(() => {
const { getMetricsFor } = result.current
const res = getMetricsFor('nodeId')
expect(res).toStrictEqual<MetricDefinitionSpec[]>([
{
chart: ChartType.SAMPLE,
metrics: 'metricName1',
source: 'nodeId',
},
{
chart: ChartType.SAMPLE,
metrics: 'metricName2',
source: 'nodeId',
},
])
})
})

it('should reset the store', async () => {
const { result } = renderHook<MetricDefinitionStore, unknown>(useMetricsStore)
const { metrics } = result.current

expect(metrics).toHaveLength(0)

act(() => {
const { addMetrics } = result.current
addMetrics('nodeId', 'metricName1', ChartType.SAMPLE)
addMetrics('nodeId', 'metricName2', ChartType.SAMPLE)
addMetrics('anotherNodeId', 'metricName1', ChartType.SAMPLE)
})

expect(result.current.metrics).toHaveLength(3)

act(() => {
const { reset } = result.current
reset()
})

expect(result.current.metrics).toHaveLength(0)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'

import { ChartType } from '../types.ts'

export interface MetricDefinitionSpec {
source: string
metrics: string
chart?: ChartType
}

export interface MetricDefinitionStore {
metrics: MetricDefinitionSpec[]
reset: () => void
addMetrics: (source: string, metrics: string, chart?: ChartType) => void
removeMetrics: (source: string, metrics: string) => void
getMetricsFor: (source: string) => MetricDefinitionSpec[]
}

const useMetricsStore = create<MetricDefinitionStore>()(
persist(
(set, get) => ({
metrics: [],
reset: () => {
set({ metrics: [] })
},
addMetrics: (source: string, metrics, chart) => {
set({
metrics: [...get().metrics, { source: source, metrics: metrics, chart: chart }],
})
},
removeMetrics: (source: string, metrics: string) => {
set({
metrics: get().metrics.filter((e) => e.source !== source || e.metrics !== metrics),
})
},
getMetricsFor: (source: string) => {
return get().metrics.filter((m) => m.source === source)
},
}),
{
name: 'edge.observability',
storage: createJSONStorage(() => localStorage),
}
)
)

export default useMetricsStore
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ const NodeGroup: FC<NodeProps<Group>> = ({ id, data, selected, ...props }) => {
onEdgesChange(edges.filter((e) => e.source === id).map((e) => ({ id: e.id, type: 'remove' } as EdgeRemoveChange)))
}

console.log('XXXXXXX data', data)
return (
<>
<NodeToolbar
Expand Down

0 comments on commit 3fc2f3c

Please sign in to comment.