mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: custom metrics (#10022)
This commit is contained in:
		
							parent
							
								
									76b201e40e
								
							
						
					
					
						commit
						9fca29f254
					
				| @ -0,0 +1,227 @@ | ||||
| import { Line } from 'react-chartjs-2'; | ||||
| import type { IMetricsCount } from 'hooks/api/getters/useMetricCounters/useMetricCounters'; | ||||
| import type {} from 'chart.js'; | ||||
| import type { Theme } from '@mui/material/styles/createTheme'; | ||||
| import { | ||||
|     useLocationSettings, | ||||
|     type ILocationSettings, | ||||
| } from 'hooks/useLocationSettings'; | ||||
| import { formatDateHM } from 'utils/formatDate'; | ||||
| import { useMemo } from 'react'; | ||||
| import { useTheme } from '@mui/material'; | ||||
| import { CyclicIterator } from 'utils/cyclicIterator'; | ||||
| import { | ||||
|     CategoryScale, | ||||
|     Chart as ChartJS, | ||||
|     type ChartDataset, | ||||
|     type ChartOptions, | ||||
|     Legend, | ||||
|     LinearScale, | ||||
|     LineElement, | ||||
|     PointElement, | ||||
|     TimeScale, | ||||
|     Title, | ||||
|     Tooltip, | ||||
| } from 'chart.js'; | ||||
| 
 | ||||
| type ChartDatasetType = ChartDataset<'line', IPoint[]>; | ||||
| 
 | ||||
| export interface IPoint { | ||||
|     x: Date; | ||||
|     y: number; | ||||
| } | ||||
| 
 | ||||
| ChartJS.register( | ||||
|     CategoryScale, | ||||
|     LinearScale, | ||||
|     PointElement, | ||||
|     LineElement, | ||||
|     TimeScale, | ||||
|     Legend, | ||||
|     Tooltip, | ||||
|     Title, | ||||
| ); | ||||
| 
 | ||||
| export const createChartOptions = ( | ||||
|     theme: Theme, | ||||
|     metrics: IMetricsCount[], | ||||
|     hoursBack: number, | ||||
|     locationSettings: ILocationSettings, | ||||
| ): ChartOptions<'line'> => { | ||||
|     return { | ||||
|         locale: locationSettings.locale, | ||||
|         responsive: true, | ||||
|         maintainAspectRatio: false, | ||||
|         interaction: { | ||||
|             mode: 'index', | ||||
|             intersect: false, | ||||
|         }, | ||||
|         color: theme.palette.text.secondary, | ||||
|         scales: { | ||||
|             y: { | ||||
|                 type: 'linear', | ||||
|                 title: { | ||||
|                     display: true, | ||||
|                     text: 'Count', | ||||
|                     color: theme.palette.text.secondary, | ||||
|                 }, | ||||
|                 suggestedMin: 0, | ||||
|                 ticks: { precision: 0, color: theme.palette.text.secondary }, | ||||
|                 grid: { | ||||
|                     color: theme.palette.divider, | ||||
|                     borderColor: theme.palette.divider, | ||||
|                 }, | ||||
|             }, | ||||
|             x: { | ||||
|                 type: 'time', | ||||
|                 time: { unit: 'minute' }, | ||||
|                 grid: { display: false }, | ||||
|                 ticks: { | ||||
|                     callback: (_, i, data) => | ||||
|                         formatDateHM(data[i].value, locationSettings.locale), | ||||
|                     color: theme.palette.text.secondary, | ||||
|                 }, | ||||
|             }, | ||||
|         }, | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| export function buildMetricKey({ name, labels }: IMetricsCount): string { | ||||
|     if (!labels || Object.keys(labels).length === 0) { | ||||
|         return encodeURIComponent(name); | ||||
|     } | ||||
| 
 | ||||
|     const labelParts = Object.entries(labels) | ||||
|         .filter(([, v]) => v != null && v !== '') // robustness: ignore empties
 | ||||
|         .sort(([a], [b]) => a.localeCompare(b)) // robustness: deterministic order
 | ||||
|         .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`); | ||||
| 
 | ||||
|     return [encodeURIComponent(name), ...labelParts].join('_'); | ||||
| } | ||||
| 
 | ||||
| const createChartPoint = (value: number, timestamp: Date): IPoint => { | ||||
|     return { | ||||
|         x: timestamp, | ||||
|         y: value, | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| class ItemPicker<T> { | ||||
|     private items: CyclicIterator<T>; | ||||
|     private picked: Map<string, T> = new Map(); | ||||
|     constructor(items: T[]) { | ||||
|         this.items = new CyclicIterator<T>(items); | ||||
|     } | ||||
| 
 | ||||
|     public pick(key: string): T { | ||||
|         if (!this.picked.has(key)) { | ||||
|             this.picked.set(key, this.items.next()); | ||||
|         } | ||||
|         return this.picked.get(key)!; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| const toValues = ( | ||||
|     metrics: IMetricsCount[], | ||||
|     selectedLabels: string[], | ||||
|     selectedValues: string[], | ||||
| ) => { | ||||
|     return metrics.reduce( | ||||
|         (acc, metric) => { | ||||
|             for (const [key, value] of Object.entries(metric.labels)) { | ||||
|                 const labelKey = buildMetricKey(metric); | ||||
|                 if (!acc[labelKey]) { | ||||
|                     acc[labelKey] = []; | ||||
|                 } | ||||
|                 acc[labelKey].push({ | ||||
|                     count: metric.value, | ||||
|                     timestamp: metric.timestamp, | ||||
|                 }); | ||||
|             } | ||||
|             return acc; | ||||
|         }, | ||||
|         {} as Record<string, { count: number; timestamp: Date }[]>, | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| export const createChartData = ( | ||||
|     theme: Theme, | ||||
|     metrics: IMetricsCount[], | ||||
|     selectedLabels: string[], | ||||
|     selectedLabelValues: string[], | ||||
|     locationSettings: ILocationSettings, | ||||
| ): ChartDatasetType[] => { | ||||
|     const colorPicker = new ItemPicker([ | ||||
|         theme.palette.success, | ||||
|         theme.palette.error, | ||||
|         theme.palette.primary, | ||||
|         theme.palette.warning, | ||||
|         theme.palette.info, | ||||
|         theme.palette.secondary, | ||||
|     ]); | ||||
|     const labelValues = toValues(metrics, selectedLabels, selectedLabelValues); | ||||
|     const datasets = Object.entries(labelValues).map(([key, values]) => { | ||||
|         const color = colorPicker.pick(key); | ||||
|         return { | ||||
|             label: key, | ||||
|             data: values.map((value) => | ||||
|                 createChartPoint(value.count, value.timestamp), | ||||
|             ), | ||||
|             borderColor: color.main, | ||||
|             backgroundColor: color.main, | ||||
|             fill: false, | ||||
|             tension: 0.1, | ||||
|             pointRadius: 0, | ||||
|             pointHoverRadius: 5, | ||||
|         }; | ||||
|     }); | ||||
|     return datasets; | ||||
| }; | ||||
| 
 | ||||
| export const ExploreCounterChart = ({ | ||||
|     selectedLabels, | ||||
|     selectedLabelValues, | ||||
|     filteredCounters, | ||||
|     counter, | ||||
|     setCounter, | ||||
| }: { | ||||
|     selectedLabels: string[]; | ||||
|     selectedLabelValues: string[]; | ||||
|     filteredCounters: IMetricsCount[]; | ||||
|     counter: string | undefined; | ||||
|     setCounter: (counter: string) => void; | ||||
| }) => { | ||||
|     const theme = useTheme(); | ||||
|     const { locationSettings } = useLocationSettings(); | ||||
| 
 | ||||
|     const options = useMemo(() => { | ||||
|         return createChartOptions( | ||||
|             theme, | ||||
|             filteredCounters, | ||||
|             10, | ||||
|             locationSettings, | ||||
|         ); | ||||
|     }, [theme, filteredCounters, locationSettings]); | ||||
| 
 | ||||
|     const data = useMemo(() => { | ||||
|         return { | ||||
|             datasets: createChartData( | ||||
|                 theme, | ||||
|                 filteredCounters, | ||||
|                 selectedLabels, | ||||
|                 selectedLabelValues, | ||||
|                 locationSettings, | ||||
|             ), | ||||
|         }; | ||||
|     }, [theme, filteredCounters, locationSettings]); | ||||
| 
 | ||||
|     return ( | ||||
|         <div style={{ height: 400 }}> | ||||
|             <Line | ||||
|                 options={options} | ||||
|                 data={data} | ||||
|                 aria-label='Explore Counters Metrics Chart' | ||||
|             /> | ||||
|         </div> | ||||
|     ); | ||||
| }; | ||||
| @ -0,0 +1,73 @@ | ||||
| import { | ||||
|     FormControl, | ||||
|     Grid, | ||||
|     InputLabel, | ||||
|     MenuItem, | ||||
|     Select, | ||||
|     type SelectChangeEvent, | ||||
| } from '@mui/material'; | ||||
| import { SelectCounterLabel } from './SelectCounterLabel.js'; | ||||
| 
 | ||||
| interface IExploreCounterFilter { | ||||
|     counter: string | undefined; | ||||
|     setCounter: (counter: string) => void; | ||||
|     counterNames: string[] | undefined; | ||||
|     labels: Record<string, string[]> | undefined; | ||||
|     selectLabel: (label: string) => void; | ||||
|     unselectLabel: (label: string) => void; | ||||
|     selectLabelValue: (value: string) => void; | ||||
|     unselectLabelValue: (value: string) => void; | ||||
| } | ||||
| 
 | ||||
| export const ExploreCounterFilter = ({ | ||||
|     counterNames, | ||||
|     labels, | ||||
|     counter, | ||||
|     setCounter, | ||||
|     selectLabel, | ||||
|     unselectLabel, | ||||
|     selectLabelValue, | ||||
|     unselectLabelValue, | ||||
| }: IExploreCounterFilter) => { | ||||
|     const counterChanged = (event: SelectChangeEvent<string>) => { | ||||
|         const selectedCounter = event.target.value as string; | ||||
|         setCounter(selectedCounter); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|         <Grid container spacing={2}> | ||||
|             <Grid item xs={12} md={6}> | ||||
|                 <Grid container spacing={2}> | ||||
|                     <FormControl> | ||||
|                         <InputLabel id='counter-label' size='small'> | ||||
|                             Counter | ||||
|                         </InputLabel> | ||||
|                         <Select | ||||
|                             label='Counter' | ||||
|                             labelId='counter-label' | ||||
|                             id='counter-select' | ||||
|                             value={counter} | ||||
|                             onChange={counterChanged} | ||||
|                             variant='outlined' | ||||
|                             size='small' | ||||
|                             sx={{ width: 200, maxWidth: '100%' }} | ||||
|                         > | ||||
|                             {counterNames?.map((option) => ( | ||||
|                                 <MenuItem key={option} value={option}> | ||||
|                                     {option} | ||||
|                                 </MenuItem> | ||||
|                             ))} | ||||
|                         </Select> | ||||
|                     </FormControl> | ||||
|                     <SelectCounterLabel | ||||
|                         labels={labels} | ||||
|                         selectLabel={selectLabel} | ||||
|                         unselectLabel={unselectLabel} | ||||
|                         selectLabelValue={selectLabelValue} | ||||
|                         unselectLabelValue={unselectLabelValue} | ||||
|                     /> | ||||
|                 </Grid> | ||||
|             </Grid> | ||||
|         </Grid> | ||||
|     ); | ||||
| }; | ||||
| @ -0,0 +1,137 @@ | ||||
| import { PageContent } from 'component/common/PageContent/PageContent'; | ||||
| import { PageHeader } from 'component/common/PageHeader/PageHeader'; | ||||
| import { | ||||
|     type IMetricsCount, | ||||
|     useMetricCounters, | ||||
| } from 'hooks/api/getters/useMetricCounters/useMetricCounters'; | ||||
| import { usePageTitle } from 'hooks/usePageTitle'; | ||||
| import { useEffect, useMemo, useState } from 'react'; | ||||
| import { ExploreCounterFilter } from './ExploreCounterFilter.js'; | ||||
| import { ExploreCounterChart } from './ExploreCounterChart.js'; | ||||
| 
 | ||||
| const mapCounterNames = (metrics: IMetricsCount[]) => { | ||||
|     return metrics.reduce((acc, metric) => { | ||||
|         if (!acc.includes(metric.name)) { | ||||
|             acc.push(metric.name); | ||||
|         } | ||||
|         return acc; | ||||
|     }, [] as string[]); | ||||
| }; | ||||
| 
 | ||||
| const mapLabels = (metrics: IMetricsCount[]) => { | ||||
|     return metrics.reduce( | ||||
|         (acc, metric) => { | ||||
|             for (const [key, value] of Object.entries(metric.labels)) { | ||||
|                 if (!acc[key]) { | ||||
|                     acc[key] = []; | ||||
|                 } | ||||
|                 if (!acc[key].includes(value)) { | ||||
|                     acc[key].push(value); | ||||
|                 } | ||||
|             } | ||||
|             return acc; | ||||
|         }, | ||||
|         {} as Record<string, string[]>, | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| export const ExploreCounters = () => { | ||||
|     usePageTitle('Explore custom metrics'); | ||||
|     const data = useMetricCounters(); | ||||
|     const [counter, setCounter] = useState<string | undefined>(undefined); | ||||
|     const [counterNames, setCounterNames] = useState<string[] | undefined>( | ||||
|         undefined, | ||||
|     ); | ||||
|     const [labels, setLabels] = useState<Record<string, string[]> | undefined>( | ||||
|         undefined, | ||||
|     ); | ||||
|     const [selectedLabels, setSelectedLabels] = useState<string[]>([]); | ||||
|     const [selectedLabelValues, setSelectedLabelValues] = useState<string[]>( | ||||
|         [], | ||||
|     ); | ||||
|     const filteredCounters = useMemo(() => { | ||||
|         return data?.counters?.metrics?.filter((metric) => { | ||||
|             if (counter && metric.name !== counter) { | ||||
|                 return false; | ||||
|             } | ||||
|             if (selectedLabels.length > 0) { | ||||
|                 const labels = Object.keys(metric.labels); | ||||
|                 return selectedLabels.every( | ||||
|                     (label) => | ||||
|                         labels.includes(label) && | ||||
|                         (selectedLabelValues.length === 0 || | ||||
|                             selectedLabelValues.includes( | ||||
|                                 `${label}::${metric.labels[label]}`, | ||||
|                             )), | ||||
|                 ); | ||||
|             } | ||||
|             return false; | ||||
|         }); | ||||
|     }, [data, counter, selectedLabels, selectedLabelValues]); | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         setCounterNames(mapCounterNames(data.counters.metrics)); | ||||
|         const labelMetrics = data.counters.metrics.filter((metric) => { | ||||
|             return counter && metric.name === counter; | ||||
|         }); | ||||
|         const counterLabels = mapLabels(labelMetrics); | ||||
|         setLabels(counterLabels); | ||||
|     }, [data.counters, filteredCounters, counter]); | ||||
| 
 | ||||
|     const selectLabel = (label: string) => { | ||||
|         setSelectedLabels((prev) => { | ||||
|             if (prev.includes(label)) { | ||||
|                 return prev.filter((l) => l !== label); | ||||
|             } | ||||
|             return [...prev, label]; | ||||
|         }); | ||||
|     }; | ||||
|     const unselectLabel = (label: string) => { | ||||
|         setSelectedLabels((prev) => { | ||||
|             return prev.filter((l) => l !== label); | ||||
|         }); | ||||
|     }; | ||||
|     const selectLabelValue = (label: string) => { | ||||
|         setSelectedLabelValues((prev) => { | ||||
|             if (prev.includes(label)) { | ||||
|                 return prev.filter((l) => l !== label); | ||||
|             } | ||||
|             return [...prev, label]; | ||||
|         }); | ||||
|     }; | ||||
|     const unselectLabelValue = (label: string) => { | ||||
|         setSelectedLabelValues((prev) => { | ||||
|             return prev.filter((l) => l !== label); | ||||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|         <> | ||||
|             <PageContent | ||||
|                 header={<PageHeader title={`Explore custom metrics`} />} | ||||
|             > | ||||
|                 <ExploreCounterFilter | ||||
|                     selectLabel={selectLabel} | ||||
|                     unselectLabel={unselectLabel} | ||||
|                     selectLabelValue={selectLabelValue} | ||||
|                     unselectLabelValue={unselectLabelValue} | ||||
|                     counter={counter} | ||||
|                     setCounter={setCounter} | ||||
|                     counterNames={counterNames} | ||||
|                     labels={labels} | ||||
|                 /> | ||||
|                 {counter && | ||||
|                     selectedLabels.length > 0 && | ||||
|                     filteredCounters.length > 0 && ( | ||||
|                         <ExploreCounterChart | ||||
|                             selectedLabels={selectedLabels} | ||||
|                             selectedLabelValues={selectedLabelValues} | ||||
|                             filteredCounters={filteredCounters} | ||||
|                             counter={counter} | ||||
|                             setCounter={setCounter} | ||||
|                         /> | ||||
|                     )} | ||||
|             </PageContent> | ||||
|         </> | ||||
|     ); | ||||
| }; | ||||
| @ -0,0 +1,107 @@ | ||||
| import { | ||||
|     FormControl, | ||||
|     InputLabel, | ||||
|     MenuItem, | ||||
|     Select, | ||||
|     type SelectChangeEvent, | ||||
| } from '@mui/material'; | ||||
| import { useState } from 'react'; | ||||
| 
 | ||||
| const getLabelNames = (labels: Record<string, string[]>) => { | ||||
|     return Object.keys(labels); | ||||
| }; | ||||
| 
 | ||||
| const getLabelValues = (label: string, labels: Record<string, string[]>) => { | ||||
|     return labels[label]; | ||||
| }; | ||||
| 
 | ||||
| export const SelectCounterLabel = ({ | ||||
|     labels, | ||||
|     unselectLabel, | ||||
|     selectLabel, | ||||
|     unselectLabelValue, | ||||
|     selectLabelValue, | ||||
| }: { | ||||
|     labels: Record<string, string[]> | undefined; | ||||
|     unselectLabel: (label: string) => void; | ||||
|     selectLabel: (label: string) => void; | ||||
|     unselectLabelValue: (labelValue: string) => void; | ||||
|     selectLabelValue: (labelValue: string) => void; | ||||
| }) => { | ||||
|     const [label, setLabel] = useState<string | undefined>(undefined); | ||||
|     const [labelValue, setLabelValue] = useState<string | undefined>(undefined); | ||||
|     const labelChanged = (event: SelectChangeEvent<string>) => { | ||||
|         unselectLabel(label as string); | ||||
|         selectLabel(event.target.value as string); | ||||
|         const selectedLabel = event.target.value as string; | ||||
|         setLabel(selectedLabel); | ||||
|     }; | ||||
|     const labelValueChanged = (event: SelectChangeEvent<string>) => { | ||||
|         unselectLabelValue(labelValue as string); | ||||
|         const newValue = event.target.value as string; | ||||
|         if (newValue === '') { | ||||
|             setLabelValue(undefined); | ||||
|             return; | ||||
|         } | ||||
|         selectLabelValue(newValue); | ||||
|         setLabelValue(newValue); | ||||
|     }; | ||||
|     return ( | ||||
|         <> | ||||
|             <FormControl> | ||||
|                 <InputLabel id='labels-label' size='small'> | ||||
|                     Label | ||||
|                 </InputLabel> | ||||
|                 <Select | ||||
|                     label='Label' | ||||
|                     labelId='labels-label' | ||||
|                     id='label-select' | ||||
|                     value={label} | ||||
|                     onChange={labelChanged} | ||||
|                     variant='outlined' | ||||
|                     size='small' | ||||
|                     sx={{ width: 200, maxWidth: '100%' }} | ||||
|                 > | ||||
|                     {labels | ||||
|                         ? getLabelNames(labels)?.map((option) => ( | ||||
|                               <MenuItem key={option} value={option}> | ||||
|                                   {option} | ||||
|                               </MenuItem> | ||||
|                           )) | ||||
|                         : null} | ||||
|                 </Select> | ||||
|             </FormControl> | ||||
|             {label ? ( | ||||
|                 <FormControl> | ||||
|                     <InputLabel id='label-value-label' size='small'> | ||||
|                         Label value | ||||
|                     </InputLabel> | ||||
|                     <Select | ||||
|                         label='Label value' | ||||
|                         labelId='label-value-label' | ||||
|                         id='label-value-select' | ||||
|                         value={labelValue} | ||||
|                         onChange={labelValueChanged} | ||||
|                         variant='outlined' | ||||
|                         size='small' | ||||
|                         sx={{ width: 200, maxWidth: '100%' }} | ||||
|                     > | ||||
|                         <MenuItem key='all' value=''> | ||||
|                             All | ||||
|                         </MenuItem> | ||||
|                         {labels | ||||
|                             ? getLabelValues(label, labels)?.map((option) => ( | ||||
|                                   <MenuItem | ||||
|                                       key={option} | ||||
|                                       value={`${label}::${option}`} | ||||
|                                   > | ||||
|                                       {option} | ||||
|                                   </MenuItem> | ||||
|                               )) | ||||
|                             : null} | ||||
|                     </Select> | ||||
|                 </FormControl> | ||||
|             ) : null} | ||||
|         </> | ||||
|     ); | ||||
| }; | ||||
| @ -34,6 +34,7 @@ import PersonalDashboardIcon from '@mui/icons-material/DashboardOutlined'; | ||||
| import { ProjectIcon } from 'component/common/ProjectIcon/ProjectIcon'; | ||||
| import PlaygroundIcon from '@mui/icons-material/AutoFixNormal'; | ||||
| import FlagOutlinedIcon from '@mui/icons-material/FlagOutlined'; | ||||
| import RocketLaunchIcon from '@mui/icons-material/RocketLaunchOutlined'; | ||||
| 
 | ||||
| // TODO: move to routes
 | ||||
| const icons: Record< | ||||
| @ -82,6 +83,7 @@ const icons: Record< | ||||
|     '/personal': PersonalDashboardIcon, | ||||
|     '/projects': ProjectIcon, | ||||
|     '/playground': PlaygroundIcon, | ||||
|     '/custom-metrics': RocketLaunchIcon, | ||||
|     GitHub: GitHubIcon, | ||||
|     Documentation: LibraryBooksIcon, | ||||
| }; | ||||
|  | ||||
| @ -152,6 +152,16 @@ exports[`returns all baseRoutes 1`] = ` | ||||
|     "title": "Applications", | ||||
|     "type": "protected", | ||||
|   }, | ||||
|   { | ||||
|     "component": [Function], | ||||
|     "flag": "customMetrics", | ||||
|     "menu": { | ||||
|       "main": true, | ||||
|     }, | ||||
|     "path": "/custom-metrics", | ||||
|     "title": "Custom metrics", | ||||
|     "type": "protected", | ||||
|   }, | ||||
|   { | ||||
|     "component": [Function], | ||||
|     "menu": {}, | ||||
|  | ||||
| @ -50,6 +50,7 @@ import { PersonalDashboard } from '../personalDashboard/PersonalDashboard.jsx'; | ||||
| import { ReleaseManagement } from 'component/releases/ReleaseManagement/ReleaseManagement'; | ||||
| import { CreateReleasePlanTemplate } from 'component/releases/ReleasePlanTemplate/CreateReleasePlanTemplate'; | ||||
| import { EditReleasePlanTemplate } from 'component/releases/ReleasePlanTemplate/EditReleasePlanTemplate'; | ||||
| import { ExploreCounters } from 'component/counters/ExploreCounters/ExploreCounters.js'; | ||||
| 
 | ||||
| export const routes: IRoute[] = [ | ||||
|     // Splash
 | ||||
| @ -175,6 +176,16 @@ export const routes: IRoute[] = [ | ||||
|         menu: { main: true }, | ||||
|     }, | ||||
| 
 | ||||
|     // Counters
 | ||||
|     { | ||||
|         path: '/custom-metrics', | ||||
|         title: 'Custom metrics', | ||||
|         component: ExploreCounters, | ||||
|         type: 'protected', | ||||
|         menu: { main: true }, | ||||
|         flag: 'customMetrics', | ||||
|     }, | ||||
| 
 | ||||
|     // Context
 | ||||
|     { | ||||
|         path: '/context/create', | ||||
|  | ||||
| @ -0,0 +1,42 @@ | ||||
| import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR.js'; | ||||
| import { formatApiPath } from 'utils/formatPath'; | ||||
| import handleErrorResponses from '../httpErrorResponseHandler.js'; | ||||
| import { useMemo } from 'react'; | ||||
| 
 | ||||
| export interface IMetricsCount { | ||||
|     name: string; | ||||
|     value: number; | ||||
|     timestamp: Date; | ||||
|     labels: Record<string, string>; | ||||
| } | ||||
| 
 | ||||
| export interface IMetricsResponse { | ||||
|     metrics: IMetricsCount[]; | ||||
|     count: number; | ||||
|     metricNames: string[]; | ||||
| } | ||||
| 
 | ||||
| export const useMetricCounters = () => { | ||||
|     const { data, error, mutate } = useConditionalSWR<IMetricsResponse>( | ||||
|         true, | ||||
|         { metrics: [], count: 0, metricNames: [] }, | ||||
|         formatApiPath('api/admin/custom-metrics'), | ||||
|         fetcher, | ||||
|     ); | ||||
| 
 | ||||
|     return useMemo( | ||||
|         () => ({ | ||||
|             counters: data ?? { metrics: [], count: 0, metricNames: [] }, | ||||
|             loading: !error && !data, | ||||
|             refetch: () => mutate(), | ||||
|             error, | ||||
|         }), | ||||
|         [data, error, mutate], | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| const fetcher = (path: string) => { | ||||
|     return fetch(path) | ||||
|         .then(handleErrorResponses('Metric counters')) | ||||
|         .then((res) => res.json()); | ||||
| }; | ||||
| @ -91,6 +91,7 @@ export type UiFlags = { | ||||
|     registerFrontendClient?: boolean; | ||||
|     featureLinks?: boolean; | ||||
|     projectLinkTemplates?: boolean; | ||||
|     customMetrics?: boolean; | ||||
| }; | ||||
| 
 | ||||
| export interface IVersionInfo { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user