From 998ab41900ad1178b48ba88a85fa0dc801e8a547 Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Tue, 17 Jun 2025 14:57:18 +0200 Subject: [PATCH 01/13] tmp: metrics frontend --- frontend/src/component/insights/Insights.tsx | 2 + .../src/component/insights/TestComponent.tsx | 82 +++ frontend/src/component/insights/data.ts | 622 ++++++++++++++++++ 3 files changed, 706 insertions(+) create mode 100644 frontend/src/component/insights/TestComponent.tsx create mode 100644 frontend/src/component/insights/data.ts diff --git a/frontend/src/component/insights/Insights.tsx b/frontend/src/component/insights/Insights.tsx index 76ab5b3e92..a6965021c0 100644 --- a/frontend/src/component/insights/Insights.tsx +++ b/frontend/src/component/insights/Insights.tsx @@ -7,6 +7,7 @@ import { StyledContainer } from './InsightsCharts.styles.ts'; import { LifecycleInsights } from './sections/LifecycleInsights.tsx'; import { PerformanceInsights } from './sections/PerformanceInsights.tsx'; import { UserInsights } from './sections/UserInsights.tsx'; +import { TestComponent } from './TestComponent.tsx'; const StyledWrapper = styled('div')(({ theme }) => ({ paddingTop: theme.spacing(2), @@ -17,6 +18,7 @@ const NewInsights: FC = () => { + diff --git a/frontend/src/component/insights/TestComponent.tsx b/frontend/src/component/insights/TestComponent.tsx new file mode 100644 index 0000000000..38c0c4fbcc --- /dev/null +++ b/frontend/src/component/insights/TestComponent.tsx @@ -0,0 +1,82 @@ +import type { FC } from 'react'; +import { useMemo } from 'react'; +import { useTheme } from '@mui/material'; +import { LineChart } from './components/LineChart/LineChart.tsx'; +import { data } from './data.ts'; + +type TestComponentProps = {}; + +const transformTimeSeriesData = (rawData: typeof data) => { + const firstDataset = rawData[0]; + const timeseries = firstDataset.data.values; + const timestamps = timeseries[0]; + const values = timeseries[1]; + + return { + timestamps: timestamps.map((ts) => new Date(ts)), + values, + }; +}; + +export const TestComponent: FC = () => { + const theme = useTheme(); + + const chartData = useMemo(() => { + const { timestamps, values } = transformTimeSeriesData(data); + + return { + labels: timestamps, + datasets: [ + { + data: values, + borderColor: theme.palette.primary.main, + backgroundColor: theme.palette.primary.light, + // tension: 0.1, + // pointRadius: 0, + // pointHoverRadius: 5, + }, + ], + }; + }, [theme]); + + return ( + + ); +}; diff --git a/frontend/src/component/insights/data.ts b/frontend/src/component/insights/data.ts new file mode 100644 index 0000000000..1b34538233 --- /dev/null +++ b/frontend/src/component/insights/data.ts @@ -0,0 +1,622 @@ +export const data = [ + { + schema: { + refId: 'A', + meta: { + type: 'timeseries-multi', + typeVersion: [0, 0], + custom: { + resultType: 'matrix', + }, + executedQueryString: + 'Expr: sum(topk(1, instance_users{state="ACTIVE", plan=~"Pro|Enterprise"}) by(clientId))\nStep: 30m0s', + preferredVisualisationType: 'graph', + }, + name: 'sum(topk(1, instance_users{state="ACTIVE", plan=~"Pro|Enterprise"}) by(clientId))', + fields: [ + { + name: 'Time', + type: 'time', + typeInfo: { + frame: 'time.Time', + }, + config: { + interval: 1800000, + }, + }, + { + name: 'Value', + type: 'number', + typeInfo: { + frame: 'float64', + }, + labels: {}, + config: { + displayNameFromDS: + 'sum(topk(1, instance_users{state="ACTIVE", plan=~"Pro|Enterprise"}) by(clientId))', + }, + }, + ], + }, + data: { + values: [ + [ + 1747512000000, 1747513800000, 1747515600000, 1747517400000, + 1747519200000, 1747521000000, 1747522800000, 1747524600000, + 1747526400000, 1747528200000, 1747530000000, 1747531800000, + 1747533600000, 1747535400000, 1747537200000, 1747539000000, + 1747540800000, 1747542600000, 1747544400000, 1747546200000, + 1747548000000, 1747549800000, 1747551600000, 1747553400000, + 1747555200000, 1747557000000, 1747558800000, 1747560600000, + 1747562400000, 1747564200000, 1747566000000, 1747567800000, + 1747569600000, 1747571400000, 1747573200000, 1747575000000, + 1747576800000, 1747578600000, 1747580400000, 1747582200000, + 1747584000000, 1747585800000, 1747587600000, 1747589400000, + 1747591200000, 1747593000000, 1747594800000, 1747596600000, + 1747598400000, 1747600200000, 1747602000000, 1747603800000, + 1747605600000, 1747607400000, 1747609200000, 1747611000000, + 1747612800000, 1747614600000, 1747616400000, 1747618200000, + 1747620000000, 1747621800000, 1747623600000, 1747625400000, + 1747627200000, 1747629000000, 1747630800000, 1747632600000, + 1747634400000, 1747636200000, 1747638000000, 1747639800000, + 1747641600000, 1747643400000, 1747645200000, 1747647000000, + 1747648800000, 1747650600000, 1747652400000, 1747654200000, + 1747656000000, 1747657800000, 1747659600000, 1747661400000, + 1747663200000, 1747665000000, 1747666800000, 1747668600000, + 1747670400000, 1747672200000, 1747674000000, 1747675800000, + 1747677600000, 1747679400000, 1747681200000, 1747683000000, + 1747684800000, 1747686600000, 1747688400000, 1747690200000, + 1747692000000, 1747693800000, 1747695600000, 1747697400000, + 1747699200000, 1747701000000, 1747702800000, 1747704600000, + 1747706400000, 1747708200000, 1747710000000, 1747711800000, + 1747713600000, 1747715400000, 1747717200000, 1747719000000, + 1747720800000, 1747722600000, 1747724400000, 1747726200000, + 1747728000000, 1747729800000, 1747731600000, 1747733400000, + 1747735200000, 1747737000000, 1747738800000, 1747740600000, + 1747742400000, 1747744200000, 1747746000000, 1747747800000, + 1747749600000, 1747751400000, 1747753200000, 1747755000000, + 1747756800000, 1747758600000, 1747760400000, 1747762200000, + 1747764000000, 1747765800000, 1747767600000, 1747769400000, + 1747771200000, 1747773000000, 1747774800000, 1747776600000, + 1747778400000, 1747780200000, 1747782000000, 1747783800000, + 1747785600000, 1747787400000, 1747789200000, 1747791000000, + 1747792800000, 1747794600000, 1747796400000, 1747798200000, + 1747800000000, 1747801800000, 1747803600000, 1747805400000, + 1747807200000, 1747809000000, 1747810800000, 1747812600000, + 1747814400000, 1747816200000, 1747818000000, 1747819800000, + 1747821600000, 1747823400000, 1747825200000, 1747827000000, + 1747828800000, 1747830600000, 1747832400000, 1747834200000, + 1747836000000, 1747837800000, 1747839600000, 1747841400000, + 1747843200000, 1747845000000, 1747846800000, 1747848600000, + 1747850400000, 1747852200000, 1747854000000, 1747855800000, + 1747857600000, 1747859400000, 1747861200000, 1747863000000, + 1747864800000, 1747866600000, 1747868400000, 1747870200000, + 1747872000000, 1747873800000, 1747875600000, 1747877400000, + 1747879200000, 1747881000000, 1747882800000, 1747884600000, + 1747886400000, 1747888200000, 1747890000000, 1747891800000, + 1747893600000, 1747895400000, 1747897200000, 1747899000000, + 1747900800000, 1747902600000, 1747904400000, 1747906200000, + 1747908000000, 1747909800000, 1747911600000, 1747913400000, + 1747915200000, 1747917000000, 1747918800000, 1747920600000, + 1747922400000, 1747924200000, 1747926000000, 1747927800000, + 1747929600000, 1747931400000, 1747933200000, 1747935000000, + 1747936800000, 1747938600000, 1747940400000, 1747942200000, + 1747944000000, 1747945800000, 1747947600000, 1747949400000, + 1747951200000, 1747953000000, 1747954800000, 1747956600000, + 1747958400000, 1747960200000, 1747962000000, 1747963800000, + 1747965600000, 1747967400000, 1747969200000, 1747971000000, + 1747972800000, 1747974600000, 1747976400000, 1747978200000, + 1747980000000, 1747981800000, 1747983600000, 1747985400000, + 1747987200000, 1747989000000, 1747990800000, 1747992600000, + 1747994400000, 1747996200000, 1747998000000, 1747999800000, + 1748001600000, 1748003400000, 1748005200000, 1748007000000, + 1748008800000, 1748010600000, 1748012400000, 1748014200000, + 1748016000000, 1748017800000, 1748019600000, 1748021400000, + 1748023200000, 1748025000000, 1748026800000, 1748028600000, + 1748030400000, 1748032200000, 1748034000000, 1748035800000, + 1748037600000, 1748039400000, 1748041200000, 1748043000000, + 1748044800000, 1748046600000, 1748048400000, 1748050200000, + 1748052000000, 1748053800000, 1748055600000, 1748057400000, + 1748059200000, 1748061000000, 1748062800000, 1748064600000, + 1748066400000, 1748068200000, 1748070000000, 1748071800000, + 1748073600000, 1748075400000, 1748077200000, 1748079000000, + 1748080800000, 1748082600000, 1748084400000, 1748086200000, + 1748088000000, 1748089800000, 1748091600000, 1748093400000, + 1748095200000, 1748097000000, 1748098800000, 1748100600000, + 1748102400000, 1748104200000, 1748106000000, 1748107800000, + 1748109600000, 1748111400000, 1748113200000, 1748115000000, + 1748116800000, 1748118600000, 1748120400000, 1748122200000, + 1748124000000, 1748125800000, 1748127600000, 1748129400000, + 1748131200000, 1748133000000, 1748134800000, 1748136600000, + 1748138400000, 1748140200000, 1748142000000, 1748143800000, + 1748145600000, 1748147400000, 1748149200000, 1748151000000, + 1748152800000, 1748154600000, 1748156400000, 1748158200000, + 1748160000000, 1748161800000, 1748163600000, 1748165400000, + 1748167200000, 1748169000000, 1748170800000, 1748172600000, + 1748174400000, 1748176200000, 1748178000000, 1748179800000, + 1748181600000, 1748183400000, 1748185200000, 1748187000000, + 1748188800000, 1748190600000, 1748192400000, 1748194200000, + 1748196000000, 1748197800000, 1748199600000, 1748201400000, + 1748203200000, 1748205000000, 1748206800000, 1748208600000, + 1748210400000, 1748212200000, 1748214000000, 1748215800000, + 1748217600000, 1748219400000, 1748221200000, 1748223000000, + 1748224800000, 1748226600000, 1748228400000, 1748230200000, + 1748232000000, 1748233800000, 1748235600000, 1748237400000, + 1748239200000, 1748241000000, 1748242800000, 1748244600000, + 1748246400000, 1748248200000, 1748250000000, 1748251800000, + 1748253600000, 1748255400000, 1748257200000, 1748259000000, + 1748260800000, 1748262600000, 1748264400000, 1748266200000, + 1748268000000, 1748269800000, 1748271600000, 1748273400000, + 1748275200000, 1748277000000, 1748278800000, 1748280600000, + 1748282400000, 1748284200000, 1748286000000, 1748287800000, + 1748289600000, 1748291400000, 1748293200000, 1748295000000, + 1748296800000, 1748298600000, 1748300400000, 1748302200000, + 1748304000000, 1748305800000, 1748307600000, 1748309400000, + 1748311200000, 1748313000000, 1748314800000, 1748316600000, + 1748318400000, 1748320200000, 1748322000000, 1748323800000, + 1748325600000, 1748327400000, 1748329200000, 1748331000000, + 1748332800000, 1748334600000, 1748336400000, 1748338200000, + 1748340000000, 1748341800000, 1748343600000, 1748345400000, + 1748347200000, 1748349000000, 1748350800000, 1748352600000, + 1748354400000, 1748356200000, 1748358000000, 1748359800000, + 1748361600000, 1748363400000, 1748365200000, 1748367000000, + 1748368800000, 1748370600000, 1748372400000, 1748374200000, + 1748376000000, 1748377800000, 1748379600000, 1748381400000, + 1748383200000, 1748385000000, 1748386800000, 1748388600000, + 1748390400000, 1748392200000, 1748394000000, 1748395800000, + 1748397600000, 1748399400000, 1748401200000, 1748403000000, + 1748404800000, 1748406600000, 1748408400000, 1748410200000, + 1748412000000, 1748413800000, 1748415600000, 1748417400000, + 1748419200000, 1748421000000, 1748422800000, 1748424600000, + 1748426400000, 1748428200000, 1748430000000, 1748431800000, + 1748433600000, 1748435400000, 1748437200000, 1748439000000, + 1748440800000, 1748442600000, 1748444400000, 1748446200000, + 1748448000000, 1748449800000, 1748451600000, 1748453400000, + 1748455200000, 1748457000000, 1748458800000, 1748460600000, + 1748462400000, 1748464200000, 1748466000000, 1748467800000, + 1748469600000, 1748471400000, 1748473200000, 1748475000000, + 1748476800000, 1748478600000, 1748480400000, 1748482200000, + 1748484000000, 1748485800000, 1748487600000, 1748489400000, + 1748491200000, 1748493000000, 1748494800000, 1748496600000, + 1748498400000, 1748500200000, 1748502000000, 1748503800000, + 1748505600000, 1748507400000, 1748509200000, 1748511000000, + 1748512800000, 1748514600000, 1748516400000, 1748518200000, + 1748520000000, 1748521800000, 1748523600000, 1748525400000, + 1748527200000, 1748529000000, 1748530800000, 1748532600000, + 1748534400000, 1748536200000, 1748538000000, 1748539800000, + 1748541600000, 1748543400000, 1748545200000, 1748547000000, + 1748548800000, 1748550600000, 1748552400000, 1748554200000, + 1748556000000, 1748557800000, 1748559600000, 1748561400000, + 1748563200000, 1748565000000, 1748566800000, 1748568600000, + 1748570400000, 1748572200000, 1748574000000, 1748575800000, + 1748577600000, 1748579400000, 1748581200000, 1748583000000, + 1748584800000, 1748586600000, 1748588400000, 1748590200000, + 1748592000000, 1748593800000, 1748595600000, 1748597400000, + 1748599200000, 1748601000000, 1748602800000, 1748604600000, + 1748606400000, 1748608200000, 1748610000000, 1748611800000, + 1748613600000, 1748615400000, 1748617200000, 1748619000000, + 1748620800000, 1748622600000, 1748624400000, 1748626200000, + 1748628000000, 1748629800000, 1748631600000, 1748633400000, + 1748635200000, 1748637000000, 1748638800000, 1748640600000, + 1748642400000, 1748644200000, 1748646000000, 1748647800000, + 1748649600000, 1748651400000, 1748653200000, 1748655000000, + 1748656800000, 1748658600000, 1748660400000, 1748662200000, + 1748664000000, 1748665800000, 1748667600000, 1748669400000, + 1748671200000, 1748673000000, 1748674800000, 1748676600000, + 1748678400000, 1748680200000, 1748682000000, 1748683800000, + 1748685600000, 1748687400000, 1748689200000, 1748691000000, + 1748692800000, 1748694600000, 1748696400000, 1748698200000, + 1748700000000, 1748701800000, 1748703600000, 1748705400000, + 1748707200000, 1748709000000, 1748710800000, 1748712600000, + 1748714400000, 1748716200000, 1748718000000, 1748719800000, + 1748721600000, 1748723400000, 1748725200000, 1748727000000, + 1748728800000, 1748730600000, 1748732400000, 1748734200000, + 1748736000000, 1748737800000, 1748739600000, 1748741400000, + 1748743200000, 1748745000000, 1748746800000, 1748748600000, + 1748750400000, 1748752200000, 1748754000000, 1748755800000, + 1748757600000, 1748759400000, 1748761200000, 1748763000000, + 1748764800000, 1748766600000, 1748768400000, 1748770200000, + 1748772000000, 1748773800000, 1748775600000, 1748777400000, + 1748779200000, 1748781000000, 1748782800000, 1748784600000, + 1748786400000, 1748788200000, 1748790000000, 1748791800000, + 1748793600000, 1748795400000, 1748797200000, 1748799000000, + 1748800800000, 1748802600000, 1748804400000, 1748806200000, + 1748808000000, 1748809800000, 1748811600000, 1748813400000, + 1748815200000, 1748817000000, 1748818800000, 1748820600000, + 1748822400000, 1748824200000, 1748826000000, 1748827800000, + 1748829600000, 1748831400000, 1748833200000, 1748835000000, + 1748836800000, 1748838600000, 1748840400000, 1748842200000, + 1748844000000, 1748845800000, 1748847600000, 1748849400000, + 1748851200000, 1748853000000, 1748854800000, 1748856600000, + 1748858400000, 1748860200000, 1748862000000, 1748863800000, + 1748865600000, 1748867400000, 1748869200000, 1748871000000, + 1748872800000, 1748874600000, 1748876400000, 1748878200000, + 1748880000000, 1748881800000, 1748883600000, 1748885400000, + 1748887200000, 1748889000000, 1748890800000, 1748892600000, + 1748894400000, 1748896200000, 1748898000000, 1748899800000, + 1748901600000, 1748903400000, 1748905200000, 1748907000000, + 1748908800000, 1748910600000, 1748912400000, 1748914200000, + 1748916000000, 1748917800000, 1748919600000, 1748921400000, + 1748923200000, 1748925000000, 1748926800000, 1748928600000, + 1748930400000, 1748932200000, 1748934000000, 1748935800000, + 1748937600000, 1748939400000, 1748941200000, 1748943000000, + 1748944800000, 1748946600000, 1748948400000, 1748950200000, + 1748952000000, 1748953800000, 1748955600000, 1748957400000, + 1748959200000, 1748961000000, 1748962800000, 1748964600000, + 1748966400000, 1748968200000, 1748970000000, 1748971800000, + 1748973600000, 1748975400000, 1748977200000, 1748979000000, + 1748980800000, 1748982600000, 1748984400000, 1748986200000, + 1748988000000, 1748989800000, 1748991600000, 1748993400000, + 1748995200000, 1748997000000, 1748998800000, 1749000600000, + 1749002400000, 1749004200000, 1749006000000, 1749007800000, + 1749009600000, 1749011400000, 1749013200000, 1749015000000, + 1749016800000, 1749018600000, 1749020400000, 1749022200000, + 1749024000000, 1749025800000, 1749027600000, 1749029400000, + 1749031200000, 1749033000000, 1749034800000, 1749036600000, + 1749038400000, 1749040200000, 1749042000000, 1749043800000, + 1749045600000, 1749047400000, 1749049200000, 1749051000000, + 1749052800000, 1749054600000, 1749056400000, 1749058200000, + 1749060000000, 1749061800000, 1749063600000, 1749065400000, + 1749067200000, 1749069000000, 1749070800000, 1749072600000, + 1749074400000, 1749076200000, 1749078000000, 1749079800000, + 1749081600000, 1749083400000, 1749085200000, 1749087000000, + 1749088800000, 1749090600000, 1749092400000, 1749094200000, + 1749096000000, 1749097800000, 1749099600000, 1749101400000, + 1749103200000, 1749105000000, 1749106800000, 1749108600000, + 1749110400000, 1749112200000, 1749114000000, 1749115800000, + 1749117600000, 1749119400000, 1749121200000, 1749123000000, + 1749124800000, 1749126600000, 1749128400000, 1749130200000, + 1749132000000, 1749133800000, 1749135600000, 1749137400000, + 1749139200000, 1749141000000, 1749142800000, 1749144600000, + 1749146400000, 1749148200000, 1749150000000, 1749151800000, + 1749153600000, 1749155400000, 1749157200000, 1749159000000, + 1749160800000, 1749162600000, 1749164400000, 1749166200000, + 1749168000000, 1749169800000, 1749171600000, 1749173400000, + 1749175200000, 1749177000000, 1749178800000, 1749180600000, + 1749182400000, 1749184200000, 1749186000000, 1749187800000, + 1749189600000, 1749191400000, 1749193200000, 1749195000000, + 1749196800000, 1749198600000, 1749200400000, 1749202200000, + 1749204000000, 1749205800000, 1749207600000, 1749209400000, + 1749211200000, 1749213000000, 1749214800000, 1749216600000, + 1749218400000, 1749220200000, 1749222000000, 1749223800000, + 1749225600000, 1749227400000, 1749229200000, 1749231000000, + 1749232800000, 1749234600000, 1749236400000, 1749238200000, + 1749240000000, 1749241800000, 1749243600000, 1749245400000, + 1749247200000, 1749249000000, 1749250800000, 1749252600000, + 1749254400000, 1749256200000, 1749258000000, 1749259800000, + 1749261600000, 1749263400000, 1749265200000, 1749267000000, + 1749268800000, 1749270600000, 1749272400000, 1749274200000, + 1749276000000, 1749277800000, 1749279600000, 1749281400000, + 1749283200000, 1749285000000, 1749286800000, 1749288600000, + 1749290400000, 1749292200000, 1749294000000, 1749295800000, + 1749297600000, 1749299400000, 1749301200000, 1749303000000, + 1749304800000, 1749306600000, 1749308400000, 1749310200000, + 1749312000000, 1749313800000, 1749315600000, 1749317400000, + 1749319200000, 1749321000000, 1749322800000, 1749324600000, + 1749326400000, 1749328200000, 1749330000000, 1749331800000, + 1749333600000, 1749335400000, 1749337200000, 1749339000000, + 1749340800000, 1749342600000, 1749344400000, 1749346200000, + 1749348000000, 1749349800000, 1749351600000, 1749353400000, + 1749355200000, 1749357000000, 1749358800000, 1749360600000, + 1749362400000, 1749364200000, 1749366000000, 1749367800000, + 1749369600000, 1749371400000, 1749373200000, 1749375000000, + 1749376800000, 1749378600000, 1749380400000, 1749382200000, + 1749384000000, 1749385800000, 1749387600000, 1749389400000, + 1749391200000, 1749393000000, 1749394800000, 1749396600000, + 1749398400000, 1749400200000, 1749402000000, 1749403800000, + 1749405600000, 1749407400000, 1749409200000, 1749411000000, + 1749412800000, 1749414600000, 1749416400000, 1749418200000, + 1749420000000, 1749421800000, 1749423600000, 1749425400000, + 1749427200000, 1749429000000, 1749430800000, 1749432600000, + 1749434400000, 1749436200000, 1749438000000, 1749439800000, + 1749441600000, 1749443400000, 1749445200000, 1749447000000, + 1749448800000, 1749450600000, 1749452400000, 1749454200000, + 1749456000000, 1749457800000, 1749459600000, 1749461400000, + 1749463200000, 1749465000000, 1749466800000, 1749468600000, + 1749470400000, 1749472200000, 1749474000000, 1749475800000, + 1749477600000, 1749479400000, 1749481200000, 1749483000000, + 1749484800000, 1749486600000, 1749488400000, 1749490200000, + 1749492000000, 1749493800000, 1749495600000, 1749497400000, + 1749499200000, 1749501000000, 1749502800000, 1749504600000, + 1749506400000, 1749508200000, 1749510000000, 1749511800000, + 1749513600000, 1749515400000, 1749517200000, 1749519000000, + 1749520800000, 1749522600000, 1749524400000, 1749526200000, + 1749528000000, 1749529800000, 1749531600000, 1749533400000, + 1749535200000, 1749537000000, 1749538800000, 1749540600000, + 1749542400000, 1749544200000, 1749546000000, 1749547800000, + 1749549600000, 1749551400000, 1749553200000, 1749555000000, + 1749556800000, 1749558600000, 1749560400000, 1749562200000, + 1749564000000, 1749565800000, 1749567600000, 1749569400000, + 1749571200000, 1749573000000, 1749574800000, 1749576600000, + 1749578400000, 1749580200000, 1749582000000, 1749583800000, + 1749585600000, 1749587400000, 1749589200000, 1749591000000, + 1749592800000, 1749594600000, 1749596400000, 1749598200000, + 1749600000000, 1749601800000, 1749603600000, 1749605400000, + 1749607200000, 1749609000000, 1749610800000, 1749612600000, + 1749614400000, 1749616200000, 1749618000000, 1749619800000, + 1749621600000, 1749623400000, 1749625200000, 1749627000000, + 1749628800000, 1749630600000, 1749632400000, 1749634200000, + 1749636000000, 1749637800000, 1749639600000, 1749641400000, + 1749643200000, 1749645000000, 1749646800000, 1749648600000, + 1749650400000, 1749652200000, 1749654000000, 1749655800000, + 1749657600000, 1749659400000, 1749661200000, 1749663000000, + 1749664800000, 1749666600000, 1749668400000, 1749670200000, + 1749672000000, 1749673800000, 1749675600000, 1749677400000, + 1749679200000, 1749681000000, 1749682800000, 1749684600000, + 1749686400000, 1749688200000, 1749690000000, 1749691800000, + 1749693600000, 1749695400000, 1749697200000, 1749699000000, + 1749700800000, 1749702600000, 1749704400000, 1749706200000, + 1749708000000, 1749709800000, 1749711600000, 1749713400000, + 1749715200000, 1749717000000, 1749718800000, 1749720600000, + 1749722400000, 1749724200000, 1749726000000, 1749727800000, + 1749729600000, 1749731400000, 1749733200000, 1749735000000, + 1749736800000, 1749738600000, 1749740400000, 1749742200000, + 1749744000000, 1749745800000, 1749747600000, 1749749400000, + 1749751200000, 1749753000000, 1749754800000, 1749756600000, + 1749758400000, 1749760200000, 1749762000000, 1749763800000, + 1749765600000, 1749767400000, 1749769200000, 1749771000000, + 1749772800000, 1749774600000, 1749776400000, 1749778200000, + 1749780000000, 1749781800000, 1749783600000, 1749785400000, + 1749787200000, 1749789000000, 1749790800000, 1749792600000, + 1749794400000, 1749796200000, 1749798000000, 1749799800000, + 1749801600000, 1749803400000, 1749805200000, 1749807000000, + 1749808800000, 1749810600000, 1749812400000, 1749814200000, + 1749816000000, 1749817800000, 1749819600000, 1749821400000, + 1749823200000, 1749825000000, 1749826800000, 1749828600000, + 1749830400000, 1749832200000, 1749834000000, 1749835800000, + 1749837600000, 1749839400000, 1749841200000, 1749843000000, + 1749844800000, 1749846600000, 1749848400000, 1749850200000, + 1749852000000, 1749853800000, 1749855600000, 1749857400000, + 1749859200000, 1749861000000, 1749862800000, 1749864600000, + 1749866400000, 1749868200000, 1749870000000, 1749871800000, + 1749873600000, 1749875400000, 1749877200000, 1749879000000, + 1749880800000, 1749882600000, 1749884400000, 1749886200000, + 1749888000000, 1749889800000, 1749891600000, 1749893400000, + 1749895200000, 1749897000000, 1749898800000, 1749900600000, + 1749902400000, 1749904200000, 1749906000000, 1749907800000, + 1749909600000, 1749911400000, 1749913200000, 1749915000000, + 1749916800000, 1749918600000, 1749920400000, 1749922200000, + 1749924000000, 1749925800000, 1749927600000, 1749929400000, + 1749931200000, 1749933000000, 1749934800000, 1749936600000, + 1749938400000, 1749940200000, 1749942000000, 1749943800000, + 1749945600000, 1749947400000, 1749949200000, 1749951000000, + 1749952800000, 1749954600000, 1749956400000, 1749958200000, + 1749960000000, 1749961800000, 1749963600000, 1749965400000, + 1749967200000, 1749969000000, 1749970800000, 1749972600000, + 1749974400000, 1749976200000, 1749978000000, 1749979800000, + 1749981600000, 1749983400000, 1749985200000, 1749987000000, + 1749988800000, 1749990600000, 1749992400000, 1749994200000, + 1749996000000, 1749997800000, 1749999600000, 1750001400000, + 1750003200000, 1750005000000, 1750006800000, 1750008600000, + 1750010400000, 1750012200000, 1750014000000, 1750015800000, + 1750017600000, 1750019400000, 1750021200000, 1750023000000, + 1750024800000, 1750026600000, 1750028400000, 1750030200000, + 1750032000000, 1750033800000, 1750035600000, 1750037400000, + 1750039200000, 1750041000000, 1750042800000, 1750044600000, + 1750046400000, 1750048200000, 1750050000000, 1750051800000, + 1750053600000, 1750055400000, 1750057200000, 1750059000000, + 1750060800000, 1750062600000, 1750064400000, 1750066200000, + 1750068000000, 1750069800000, 1750071600000, 1750073400000, + 1750075200000, 1750077000000, 1750078800000, 1750080600000, + 1750082400000, 1750084200000, 1750086000000, 1750087800000, + 1750089600000, 1750091400000, 1750093200000, 1750095000000, + 1750096800000, 1750098600000, 1750100400000, 1750102200000, + 1750104000000, + ], + [ + 11494, 11494, 11494, 11494, 11494, 11494, 11494, 11494, + 11494, 11494, 11494, 11494, 11494, 11494, 11494, 11494, + 11494, 11494, 11494, 11494, 11494, 11494, 11494, 11494, + 11494, 11494, 11494, 11494, 11494, 11494, 11494, 11494, + 11494, 11495, 11495, 11494, 11494, 11494, 11494, 11494, + 11494, 11508, 11508, 11508, 11508, 11508, 11508, 11508, + 11508, 11508, 11508, 11508, 11508, 11508, 11508, 11508, + 11508, 11508, 11508, 11508, 11508, 11508, 11508, 11508, + 11508, 11508, 11508, 11508, 11508, 11508, 11508, 11508, + 11508, 11508, 11508, 11510, 11510, 11510, 11510, 11511, + 11511, 11511, 11511, 11510, 11510, 11511, 11511, 11508, + 11508, 11510, 11510, 11509, 11509, 11509, 11509, 11512, + 11512, 11513, 11513, 11513, 11513, 11513, 11513, 11513, + 11513, 11513, 11513, 11513, 11513, 11513, 11513, 11513, + 11513, 11513, 11513, 11513, 11513, 11513, 11513, 11513, + 11513, 11513, 11513, 11513, 11513, 11513, 11513, 11514, + 11514, 11513, 11513, 11515, 11515, 11518, 11518, 11519, + 11519, 11519, 11519, 11520, 11520, 11519, 11519, 11519, + 11519, 11519, 11519, 11519, 11519, 11520, 11520, 11520, + 11520, 11520, 11520, 11520, 11520, 11521, 11521, 11521, + 11521, 11521, 11521, 11521, 11521, 11521, 11521, 11524, + 11524, 11525, 11525, 11526, 11526, 11526, 11526, 11525, + 11525, 11527, 11527, 11531, 11531, 11533, 11533, 11534, + 11534, 11531, 11531, 11532, 11532, 11532, 11532, 11532, + 11532, 11532, 11532, 11532, 11532, 11532, 11532, 11532, + 11532, 11532, 11532, 11532, 11532, 11533, 11533, 11533, + 11533, 11533, 11533, 11533, 11533, 11534, 11534, 11534, + 11534, 11535, 11535, 11536, 11536, 11536, 11536, 11536, + 11536, 11536, 11536, 11534, 11534, 11536, 11536, 11537, + 11537, 11538, 11538, 11538, 11538, 11538, 11538, 11538, + 11538, 11538, 11538, 11538, 11538, 11538, 11538, 11538, + 11538, 11538, 11538, 11538, 11538, 11538, 11538, 11538, + 11538, 11538, 11538, 11538, 11538, 11538, 11538, 11538, + 11538, 11539, 11539, 11540, 11540, 11541, 11541, 11527, + 11527, 11529, 11529, 11531, 11531, 11532, 11532, 11530, + 11530, 11529, 11529, 11529, 11529, 11529, 11529, 11529, + 11529, 11528, 11528, 11528, 11528, 11528, 11528, 11528, + 11528, 11528, 11528, 11528, 11528, 11528, 11528, 11528, + 11528, 11528, 11528, 11528, 11528, 11528, 11528, 11528, + 11528, 11528, 11528, 11528, 11528, 11528, 11528, 11528, + 11528, 11528, 11528, 11528, 11528, 11528, 11528, 11528, + 11528, 11528, 11528, 11528, 11528, 11528, 11528, 11528, + 11528, 11528, 11528, 11528, 11528, 11528, 11528, 11528, + 11528, 11528, 11528, 11528, 11528, 11528, 11528, 11528, + 11528, 11528, 11528, 11527, 11527, 11527, 11527, 11527, + 11527, 11527, 11527, 11527, 11527, 11527, 11527, 11527, + 11527, 11526, 11526, 11519, 11519, 11512, 11512, 11512, + 11512, 11512, 11512, 11512, 11512, 11512, 11512, 11512, + 11512, 11512, 11512, 11512, 11512, 11512, 11512, 11512, + 11512, 11512, 11512, 11512, 11512, 11512, 11512, 11511, + 11511, 11511, 11511, 11511, 11511, 11510, 11510, 11513, + 11513, 11513, 11513, 11513, 11513, 11514, 11514, 11514, + 11514, 11514, 11514, 11515, 11515, 11519, 11519, 11520, + 11520, 11519, 11519, 11518, 11518, 11517, 11517, 11518, + 11518, 11518, 11518, 11518, 11518, 11518, 11518, 11518, + 11518, 11519, 11519, 11519, 11519, 11519, 11519, 11519, + 11519, 11519, 11519, 11519, 11519, 11520, 11520, 11515, + 11515, 11516, 11516, 11518, 11518, 11520, 11520, 11521, + 11521, 11522, 11522, 11525, 11526, 11526, 11531, 11531, + 11531, 11531, 11531, 11531, 11531, 11531, 11532, 11532, + 11534, 11534, 11534, 11534, 11535, 11535, 11535, 11535, + 11535, 11535, 11535, 11535, 11535, 11535, 11535, 11535, + 11535, 11535, 11535, 11535, 11535, 11535, 11535, 11535, + 11536, 11536, 11535, 11535, 11536, 11536, 11538, 11538, + 11538, 11538, 11539, 11539, 11542, 11542, 11544, 11544, + 11544, 11544, 11544, 11544, 11548, 11548, 11550, 11550, + 11549, 11549, 11549, 11549, 11549, 11549, 11549, 11549, + 11550, 11550, 11550, 11550, 11550, 11550, 11550, 11550, + 11550, 11550, 11550, 11550, 11549, 11549, 11549, 11549, + 11550, 11550, 11551, 11551, 11551, 11551, 11551, 11551, + 11552, 11552, 11552, 11552, 11552, 11552, 11537, 11537, + 11537, 11537, 11536, 11536, 11537, 11537, 11537, 11537, + 11538, 11538, 11539, 11539, 11539, 11539, 11539, 11539, + 11539, 11539, 11539, 11539, 11539, 11539, 11539, 11539, + 11539, 11539, 11539, 11539, 11539, 11539, 11539, 11539, + 11538, 11538, 11537, 11537, 11537, 11537, 11537, 11536, + 11536, 11537, 11537, 11535, 11535, 11530, 11530, 11530, + 11530, 11530, 11530, 11525, 11525, 11525, 11525, 11519, + 11519, 11517, 11517, 11517, 11517, 11517, 11517, 11517, + 11517, 11517, 11517, 11517, 11517, 11517, 11517, 11517, + 11517, 11517, 11517, 11517, 11517, 11517, 11517, 11517, + 11517, 11517, 11517, 11517, 11517, 11517, 11517, 11517, + 11517, 11517, 11517, 11517, 11517, 11517, 11517, 11517, + 11517, 11517, 11517, 11517, 11517, 11517, 11517, 11517, + 11517, 11517, 11517, 11517, 11517, 11517, 11517, 11517, + 11517, 11517, 11517, 11517, 11517, 11517, 11517, 11517, + 11517, 11517, 11517, 11517, 11517, 11517, 11517, 11517, + 11517, 11517, 11517, 11517, 11517, 11517, 11517, 11517, + 11517, 11517, 11517, 11517, 11517, 11518, 11518, 11518, + 11518, 11518, 11518, 11518, 11518, 11518, 11518, 11518, + 11518, 11518, 11518, 11518, 11518, 11518, 11518, 11518, + 11518, 11518, 11518, 11518, 11518, 11518, 11518, 11518, + 11518, 11518, 11518, 11518, 11518, 11518, 11518, 11518, + 11518, 11518, 11518, 11519, 11519, 11513, 11513, 11513, + 11513, 11513, 11513, 11517, 11517, 11513, 11513, 11515, + 11515, 11514, 11514, 11519, 11519, 11520, 11520, 11520, + 11520, 11519, 11519, 11519, 11519, 11519, 11519, 11519, + 11519, 11519, 11519, 11519, 11519, 11518, 11518, 11518, + 11518, 11519, 11519, 11519, 11519, 11517, 11517, 11517, + 11518, 11520, 11519, 11519, 11521, 11522, 11522, 11522, + 11522, 11522, 11522, 11521, 11521, 11521, 11528, 11528, + 11519, 11519, 11518, 11518, 11520, 11520, 11522, 11522, + 11524, 11524, 11524, 11524, 11524, 11524, 11525, 11525, + 11525, 11525, 11525, 11525, 11525, 11525, 11525, 11525, + 11525, 11525, 11525, 11525, 11525, 11525, 11529, 11529, + 11493, 11493, 11494, 11494, 11498, 11498, 11499, 11499, + 11499, 11499, 11499, 11499, 11500, 11500, 11500, 11500, + 11502, 11502, 11504, 11504, 11507, 11507, 11517, 11517, + 11518, 11518, 11517, 11517, 11517, 11517, 11517, 11517, + 11517, 11517, 11517, 11517, 11517, 11517, 11517, 11517, + 11517, 11517, 11517, 11517, 11517, 11517, 11517, 11517, + 11520, 11520, 11520, 11520, 11521, 11521, 11521, 11521, + 11522, 11522, 11523, 11523, 11525, 11525, 11524, 11524, + 11526, 11526, 11526, 11526, 11526, 11526, 11527, 11527, + 11525, 11525, 11525, 11525, 11525, 11525, 11521, 11521, + 11521, 11521, 11521, 11521, 11521, 11521, 11521, 11521, + 11521, 11521, 11521, 11521, 11522, 11522, 11523, 11523, + 11523, 11523, 11524, 11524, 11515, 11515, 11516, 11516, + 11516, 11516, 11516, 11516, 11517, 11517, 11521, 11521, + 11427, 11427, 11427, 11427, 11428, 11428, 11428, 11428, + 11428, 11428, 11427, 11427, 11427, 11427, 11428, 11428, + 11428, 11428, 11428, 11428, 11428, 11428, 11428, 11428, + 11428, 11428, 11428, 11428, 11427, 11427, 11427, 11427, + 11427, 11427, 11427, 11427, 11427, 11427, 11427, 11427, + 11427, 11427, 11427, 11427, 11427, 11427, 11427, 11427, + 11427, 11427, 11427, 11427, 11427, 11427, 11427, 11427, + 11427, 11427, 11427, 11427, 11427, 11427, 11427, 11427, + 11427, 11427, 11427, 11427, 11427, 11427, 11427, 11427, + 11427, 11427, 11427, 11427, 11427, 11427, 11427, 11427, + 11427, 11427, 11427, 11427, 11427, 11427, 11427, 11427, + 11427, 11427, 11427, 11427, 11427, 11427, 11426, 11426, + 11426, 11426, 11425, 11425, 11425, 11425, 11425, 11425, + 11425, 11425, 11425, 11425, 11425, 11425, 11425, 11425, + 11425, 11425, 11425, 11425, 11425, 11425, 11425, 11425, + 11425, 11425, 11425, 11425, 11426, 11426, 11426, 11426, + 11427, 11427, 11429, 11429, 11430, 11431, 11431, 11432, + 11432, 11432, 11432, 11435, 11435, 11436, 11436, 11438, + 11438, 11438, 11438, 11439, 11439, 11437, 11437, 11441, + 11441, 11443, 11443, 11438, 11438, 11438, 11438, 11438, + 11438, 11439, 11439, 11439, 11439, 11439, 11439, 11439, + 11439, 11440, 11440, 11442, 11442, 11443, 11443, 11443, + 11443, 11443, 11443, 11439, 11439, 11447, 11447, 11447, + 11447, 11447, 11447, 11449, 11449, 11455, 11455, 11457, + 11458, 11459, 11458, 11458, 11459, 11459, 11459, 11459, + 11471, 11471, 11471, 11471, 11471, 11471, 11471, 11471, + 11472, 11472, 11472, 11472, 11472, 11472, 11472, 11472, + 11472, 11472, 11472, 11472, 11472, 11472, 11470, 11471, + 11472, 11472, 11472, 11473, 11473, 11461, 11463, 11464, + 11464, 11465, 11465, 11465, 11464, 11464, 11465, 11465, + 11466, 11466, 11469, 11469, 11471, 11471, 11468, 11468, + 11468, 11468, 11468, 11468, 11469, 11469, 11470, 11470, + 11470, 11470, 11470, 11470, 11470, 11470, 11470, 11470, + 11470, 11470, 11470, 11470, 11470, 11470, 11470, 11470, + 11470, 11470, 11470, 11466, 11466, 11469, 11469, 11471, + 11471, 11471, 11471, 11458, 11458, 11458, 11458, 11459, + 11459, 11463, 11463, 11464, 11464, 11464, 11464, 11464, + 11464, 11464, 11464, 11465, 11465, 11464, 11464, 11464, + 11464, 11464, 11464, 11465, 11465, 11454, 11454, 11454, + 11454, 11454, 11454, 11454, 11454, 11454, 11454, 11454, + 11454, 11456, 11456, 11458, 11458, 11460, 11460, 11460, + 11460, 11461, 11461, 11461, 11461, 11461, 11461, 11459, + 11459, 11461, 11461, 11462, 11462, 11462, 11462, 11464, + 11464, 11464, 11464, 11463, 11463, 11463, 11463, 11463, + 11463, 11462, 11462, 11462, 11462, 11462, 11462, 11461, + 11461, 11461, 11461, 11461, 11461, 11461, 11461, 11461, + 11461, 11461, 11461, 11461, 11461, 11461, 11461, 11461, + 11461, 11461, 11461, 11461, 11461, 11461, 11461, 11461, + 11461, 11461, 11461, 11462, 11462, 11463, 11463, 11463, + 11463, 11463, 11463, 11463, 11463, 11463, 11463, 11463, + 11463, 11463, 11463, 11463, 11463, 11463, 11463, 11463, + 11463, 11463, 11463, 11463, 11463, 11463, 11463, 11463, + 11463, 11463, 11463, 11463, 11463, 11463, 11463, 11463, + 11463, 11463, 11463, 11463, 11463, 11463, 11463, 11464, + 11464, 11464, 11464, 11464, 11464, 11464, 11464, 11464, + 11464, 11464, 11464, 11464, 11464, 11464, 11464, 11464, + 11464, 11464, 11464, 11464, 11464, 11464, 11464, 11465, + 11465, 11465, 11465, 11466, 11466, 11467, 11467, 11469, + 11469, 11469, 11469, 11472, 11472, 11472, 11473, 11473, + 11467, 11469, 11469, 11470, 11470, 11470, 11470, 11472, + 11472, 11477, 11477, 11477, 11477, 11475, 11475, 11475, + 11475, + ], + ], + }, + }, + { + schema: { + refId: 'A-Instant', + meta: { + type: 'timeseries-multi', + typeVersion: [0, 0], + custom: { + resultType: 'vector', + }, + executedQueryString: + 'Expr: sum(topk(1, instance_users{state="ACTIVE", plan=~"Pro|Enterprise"}) by(clientId))\nStep: 30m0s', + preferredVisualisationType: 'rawPrometheus', + }, + fields: [ + { + name: 'Time', + type: 'time', + config: {}, + }, + { + name: 'Value', + type: 'number', + config: {}, + }, + ], + }, + data: { + values: [[1750105009221], [11474]], + }, + }, +]; From 1d06e30a28b9058dd24ca4d17f4333e8a0ba94df Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Tue, 17 Jun 2025 20:30:25 +0200 Subject: [PATCH 02/13] demo: prometheus --- .../createImpactMetricsService.ts | 10 ++ .../impact-metrics-controller.ts | 67 ++++++++ .../impact-metrics/impact-metrics-service.ts | 155 ++++++++++++++++++ src/lib/openapi/spec/impact-metrics-schema.ts | 15 ++ src/lib/openapi/spec/index.ts | 1 + src/lib/routes/admin-api/index.ts | 5 + src/lib/services/index.ts | 11 ++ src/lib/types/experimental.ts | 4 + 8 files changed, 268 insertions(+) create mode 100644 src/lib/features/impact-metrics/createImpactMetricsService.ts create mode 100644 src/lib/features/impact-metrics/impact-metrics-controller.ts create mode 100644 src/lib/features/impact-metrics/impact-metrics-service.ts create mode 100644 src/lib/openapi/spec/impact-metrics-schema.ts diff --git a/src/lib/features/impact-metrics/createImpactMetricsService.ts b/src/lib/features/impact-metrics/createImpactMetricsService.ts new file mode 100644 index 0000000000..457a0b79b9 --- /dev/null +++ b/src/lib/features/impact-metrics/createImpactMetricsService.ts @@ -0,0 +1,10 @@ +import type { IUnleashConfig } from '../../types/index.js'; +import ImpactMetricsService from './impact-metrics-service.js'; + +export const createImpactMetricsService = (config: IUnleashConfig) => { + return new ImpactMetricsService(config); +}; + +export const createFakeImpactMetricsService = (config: IUnleashConfig) => { + return new ImpactMetricsService(config); +}; diff --git a/src/lib/features/impact-metrics/impact-metrics-controller.ts b/src/lib/features/impact-metrics/impact-metrics-controller.ts new file mode 100644 index 0000000000..bc416ec703 --- /dev/null +++ b/src/lib/features/impact-metrics/impact-metrics-controller.ts @@ -0,0 +1,67 @@ +import type { Response } from 'express'; +import Controller from '../../routes/controller.js'; +import type { IAuthRequest } from '../../routes/unleash-types.js'; +import type { IUnleashConfig } from '../../types/option.js'; +import { NONE } from '../../types/permissions.js'; +import type { OpenApiService } from '../../services/openapi-service.js'; +import { getStandardResponses } from '../../openapi/util/standard-responses.js'; +import { createResponseSchema } from '../../openapi/util/create-response-schema.js'; +import type ImpactMetricsService from './impact-metrics-service.js'; +import { + impactMetricsSchema, + type ImpactMetricsSchema, +} from '../../openapi/spec/impact-metrics-schema.js'; + +interface ImpactMetricsServices { + impactMetricsService: ImpactMetricsService; + openApiService: OpenApiService; +} + +export default class ImpactMetricsController extends Controller { + private impactMetricsService: ImpactMetricsService; + private openApiService: OpenApiService; + + constructor( + config: IUnleashConfig, + { impactMetricsService, openApiService }: ImpactMetricsServices, + ) { + super(config); + this.impactMetricsService = impactMetricsService; + this.openApiService = openApiService; + + this.route({ + method: 'get', + path: '', + handler: this.getImpactMetrics, + permission: NONE, + middleware: [ + openApiService.validPath({ + tags: ['Unstable'], + operationId: 'getImpactMetrics', + summary: 'Get impact metrics', + description: + 'Retrieves impact metrics data from Prometheus and forwards it to the frontend.', + responses: { + 200: createResponseSchema('impactMetricsSchema'), + ...getStandardResponses(401, 403, 404), + }, + }), + ], + }); + } + + async getImpactMetrics( + req: IAuthRequest, + res: Response, + ): Promise { + const metrics = + await this.impactMetricsService.getMetricsFromPrometheus(); + + this.openApiService.respondWithValidation( + 200, + res, + impactMetricsSchema.$id, + metrics, + ); + } +} diff --git a/src/lib/features/impact-metrics/impact-metrics-service.ts b/src/lib/features/impact-metrics/impact-metrics-service.ts new file mode 100644 index 0000000000..fa57886558 --- /dev/null +++ b/src/lib/features/impact-metrics/impact-metrics-service.ts @@ -0,0 +1,155 @@ +import type { Logger } from '../../logger.js'; +import type { IUnleashConfig } from '../../types/index.js'; + +interface PrometheusResponse { + status: string; + data: any; +} + +interface MetricSeries { + name: string; + available: boolean; + labels: Record; +} + +export interface IImpactMetricsService { + getMetricsFromPrometheus(): Promise<{ + availableMetrics: MetricSeries[]; + totalCount: number; + }>; +} + +// TODO: URL +const prometheusUrl = 'http://localhost:9090'; + +export default class ImpactMetricsService implements IImpactMetricsService { + private logger: Logger; + private config: IUnleashConfig; + + constructor(config: IUnleashConfig) { + this.logger = config.getLogger( + 'impact-metrics/impact-metrics-service.ts', + ); + this.config = config; + } + + async getMetricsFromPrometheus(): Promise<{ + availableMetrics: MetricSeries[]; + totalCount: number; + }> { + const testingMetrics = [ + 'node_cpu_seconds_total', + 'node_load5', + 'node_memory_MemAvailable_bytes', + 'node_disk_read_bytes_total', + 'node_disk_written_bytes_total', + 'node_network_receive_bytes_total', + 'node_network_transmit_bytes_total', + 'node_filesystem_avail_bytes', + 'node_filesystem_size_bytes', + ]; + + try { + this.logger.info('Fetching all metrics from Prometheus'); + + const metricsResponse = await fetch( + `${prometheusUrl}/api/v1/label/__name__/values`, + ); + + if (!metricsResponse.ok) { + throw new Error( + `Failed to fetch metrics: ${metricsResponse.status}`, + ); + } + + const metricsData = + (await metricsResponse.json()) as PrometheusResponse; + if (metricsData.status !== 'success') { + throw new Error('Prometheus API returned error status'); + } + + const allMetrics: string[] = metricsData.data; + this.logger.info( + `Found ${allMetrics.length} total metrics in Prometheus`, + ); + + const availableMetrics: MetricSeries[] = []; + + for (const metricName of testingMetrics) { + const isAvailable = allMetrics.includes(metricName); + + let labels: Record = {}; + + if (isAvailable) { + try { + const seriesUrl = `${prometheusUrl}/api/v1/series?match[]=${encodeURIComponent(metricName)}`; + this.logger.debug( + `Fetching series for metric: ${metricName}`, + ); + + const seriesResponse = await fetch(seriesUrl); + const seriesData = + (await seriesResponse.json()) as PrometheusResponse; + if (seriesData.status === 'success') { + const labelMap: Record> = {}; + + seriesData.data.forEach( + (series: Record) => { + Object.entries(series).forEach( + ([key, value]) => { + if (key !== '__name__') { + if (!labelMap[key]) { + labelMap[key] = new Set(); + } + labelMap[key].add(value); + } + }, + ); + }, + ); + + labels = Object.fromEntries( + Object.entries(labelMap).map( + ([key, valueSet]) => [ + key, + Array.from(valueSet).sort(), + ], + ), + ); + } + } catch (error) { + this.logger.warn( + `Failed to fetch labels for metric ${metricName}:`, + error, + ); + } + } + + availableMetrics.push({ + name: metricName, + available: isAvailable, + labels, + }); + + this.logger.debug( + `Metric ${metricName}: available=${isAvailable}, labelCount=${Object.keys(labels).length}`, + ); + } + + const availableCount = availableMetrics.filter( + (m) => m.available, + ).length; + this.logger.info( + `Found ${availableCount}/${testingMetrics.length} test metrics available in Prometheus`, + ); + + return { + availableMetrics, + totalCount: allMetrics.length, + }; + } catch (error) { + this.logger.error('Error fetching metrics from Prometheus:', error); + throw error; + } + } +} diff --git a/src/lib/openapi/spec/impact-metrics-schema.ts b/src/lib/openapi/spec/impact-metrics-schema.ts new file mode 100644 index 0000000000..6b877dfec1 --- /dev/null +++ b/src/lib/openapi/spec/impact-metrics-schema.ts @@ -0,0 +1,15 @@ +import type { FromSchema } from 'json-schema-to-ts'; + +export const impactMetricsSchema = { + $id: '#/components/schemas/impactMetricsSchema', + type: 'object', + description: 'Impact metrics data from Prometheus', + additionalProperties: true, + required: [], + properties: {}, + components: { + schemas: {}, + }, +} as const; + +export type ImpactMetricsSchema = FromSchema; diff --git a/src/lib/openapi/spec/index.ts b/src/lib/openapi/spec/index.ts index 04771b2f0e..6e44c2ba4b 100644 --- a/src/lib/openapi/spec/index.ts +++ b/src/lib/openapi/spec/index.ts @@ -113,6 +113,7 @@ export * from './health-overview-schema.js'; export * from './health-report-schema.js'; export * from './id-schema.js'; export * from './ids-schema.js'; +export * from './impact-metrics-schema.js'; export * from './import-toggles-schema.js'; export * from './import-toggles-validate-item-schema.js'; export * from './import-toggles-validate-schema.js'; diff --git a/src/lib/routes/admin-api/index.ts b/src/lib/routes/admin-api/index.ts index 5e966f699c..f26d4a0771 100644 --- a/src/lib/routes/admin-api/index.ts +++ b/src/lib/routes/admin-api/index.ts @@ -35,6 +35,7 @@ import { InactiveUsersController } from '../../users/inactive/inactive-users-con import { UiObservabilityController } from '../../features/ui-observability-controller/ui-observability-controller.js'; import { SearchApi } from './search/index.js'; import PersonalDashboardController from '../../features/personal-dashboard/personal-dashboard-controller.js'; +import ImpactMetricsController from '../../features/impact-metrics/impact-metrics-controller.js'; import FeatureLifecycleCountController from '../../features/feature-lifecycle/feature-lifecycle-count-controller.js'; import type { IUnleashServices } from '../../services/index.js'; import CustomMetricsController from '../../features/metrics/custom/custom-metrics-controller.js'; @@ -137,6 +138,10 @@ export class AdminApi extends Controller { '/personal-dashboard', new PersonalDashboardController(config, services).router, ); + this.app.use( + '/impact-metrics', + new ImpactMetricsController(config, services).router, + ); this.app.use( '/environments', new EnvironmentsController(config, services).router, diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index 0f80b77f38..19aaf35332 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -171,6 +171,11 @@ import type { import type { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType.js'; import { UnknownFlagsService } from '../features/metrics/unknown-flags/unknown-flags-service.js'; import type FeatureLinkService from '../features/feature-links/feature-link-service.js'; +import { + createFakeImpactMetricsService, + createImpactMetricsService, +} from '../features/impact-metrics/createImpactMetricsService.js'; +import type ImpactMetricsService from '../features/impact-metrics/impact-metrics-service.js'; export const createServices = ( stores: IUnleashStores, @@ -441,6 +446,10 @@ export const createServices = ( ? withTransactional(createUserSubscriptionsService(config), db) : withFakeTransactional(createFakeUserSubscriptionsService(config)); + const impactMetricsService = db + ? createImpactMetricsService(config) + : createFakeImpactMetricsService(config); + return { transactionalAccessService, accessService, @@ -507,6 +516,7 @@ export const createServices = ( integrationEventsService, onboardingService, personalDashboardService, + impactMetricsService, projectStatusService, transactionalUserSubscriptionsService, uniqueConnectionService, @@ -638,6 +648,7 @@ export interface IUnleashServices { integrationEventsService: IntegrationEventsService; onboardingService: OnboardingService; personalDashboardService: PersonalDashboardService; + impactMetricsService: ImpactMetricsService; projectStatusService: ProjectStatusService; transactionalUserSubscriptionsService: WithTransactional; uniqueConnectionService: UniqueConnectionService; diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index 41111f6c4a..cc3a22d432 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -292,6 +292,10 @@ const flags: IFlags = { process.env.UNLEASH_EXPERIMENTAL_IMPROVED_JSON_DIFF, false, ), + impactMetrics: parseEnvVarBoolean( + process.env.UNLEASH_EXPERIMENTAL_IMPACT_METRICS, + false, + ), }; export const defaultExperimentalOptions: IExperimentalOptions = { From 7bed75cb9f5b1a4f3e0fc131333bcb139bd47426 Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Wed, 18 Jun 2025 23:37:17 +0200 Subject: [PATCH 03/13] feat: add impact metrics feature with controls and data fetching --- frontend/src/component/insights/Insights.tsx | 6 +- .../src/component/insights/TestComponent.tsx | 82 ------- .../components/LineChart/LineChart.tsx | 11 +- .../insights/impact-metrics/ImpactMetrics.tsx | 206 ++++++++++++++++++ .../impact-metrics/ImpactMetricsControls.tsx | 94 ++++++++ .../insights/impact-metrics/time-utils.ts | 57 +++++ .../useImpactMetricsData.ts | 42 ++++ .../useImpactMetricsMetadata.ts | 22 ++ frontend/src/interfaces/uiConfig.ts | 1 + 9 files changed, 432 insertions(+), 89 deletions(-) delete mode 100644 frontend/src/component/insights/TestComponent.tsx create mode 100644 frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx create mode 100644 frontend/src/component/insights/impact-metrics/ImpactMetricsControls.tsx create mode 100644 frontend/src/component/insights/impact-metrics/time-utils.ts create mode 100644 frontend/src/hooks/api/getters/useImpactMetricsData/useImpactMetricsData.ts create mode 100644 frontend/src/hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata.ts diff --git a/frontend/src/component/insights/Insights.tsx b/frontend/src/component/insights/Insights.tsx index a6965021c0..f5936916d9 100644 --- a/frontend/src/component/insights/Insights.tsx +++ b/frontend/src/component/insights/Insights.tsx @@ -7,18 +7,20 @@ import { StyledContainer } from './InsightsCharts.styles.ts'; import { LifecycleInsights } from './sections/LifecycleInsights.tsx'; import { PerformanceInsights } from './sections/PerformanceInsights.tsx'; import { UserInsights } from './sections/UserInsights.tsx'; -import { TestComponent } from './TestComponent.tsx'; +import { ImpactMetrics } from './impact-metrics/ImpactMetrics.tsx'; const StyledWrapper = styled('div')(({ theme }) => ({ paddingTop: theme.spacing(2), })); const NewInsights: FC = () => { + const impactMetricsEnabled = useUiFlag('impactMetrics'); + return ( - + {impactMetricsEnabled ? : null} diff --git a/frontend/src/component/insights/TestComponent.tsx b/frontend/src/component/insights/TestComponent.tsx deleted file mode 100644 index 38c0c4fbcc..0000000000 --- a/frontend/src/component/insights/TestComponent.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import type { FC } from 'react'; -import { useMemo } from 'react'; -import { useTheme } from '@mui/material'; -import { LineChart } from './components/LineChart/LineChart.tsx'; -import { data } from './data.ts'; - -type TestComponentProps = {}; - -const transformTimeSeriesData = (rawData: typeof data) => { - const firstDataset = rawData[0]; - const timeseries = firstDataset.data.values; - const timestamps = timeseries[0]; - const values = timeseries[1]; - - return { - timestamps: timestamps.map((ts) => new Date(ts)), - values, - }; -}; - -export const TestComponent: FC = () => { - const theme = useTheme(); - - const chartData = useMemo(() => { - const { timestamps, values } = transformTimeSeriesData(data); - - return { - labels: timestamps, - datasets: [ - { - data: values, - borderColor: theme.palette.primary.main, - backgroundColor: theme.palette.primary.light, - // tension: 0.1, - // pointRadius: 0, - // pointHoverRadius: 5, - }, - ], - }; - }, [theme]); - - return ( - - ); -}; diff --git a/frontend/src/component/insights/components/LineChart/LineChart.tsx b/frontend/src/component/insights/components/LineChart/LineChart.tsx index a0262bcb72..98132bedfc 100644 --- a/frontend/src/component/insights/components/LineChart/LineChart.tsx +++ b/frontend/src/component/insights/components/LineChart/LineChart.tsx @@ -27,7 +27,10 @@ export const fillGradientPrimary = fillGradient( 'rgba(129, 122, 254, 0.12)', ); -export const NotEnoughData = () => ( +export const NotEnoughData = ({ + title = 'Not enough data', + description = 'Two or more weeks of data are needed to show a chart.', +}) => ( <> ( paddingBottom: theme.spacing(1), })} > - Not enough data - - - Two or more weeks of data are needed to show a chart. + {title} + {description} ); diff --git a/frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx b/frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx new file mode 100644 index 0000000000..bf819e5162 --- /dev/null +++ b/frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx @@ -0,0 +1,206 @@ +import type { FC } from 'react'; +import { useMemo, useState } from 'react'; +import { useTheme, Box, Typography, Alert } from '@mui/material'; +import { + LineChart, + NotEnoughData, +} from '../components/LineChart/LineChart.tsx'; +import { InsightsSection } from '../sections/InsightsSection.tsx'; +import { + StyledChartContainer, + StyledWidget, + StyledWidgetStats, +} from 'component/insights/InsightsCharts.styles'; +import { useImpactMetricsMetadata } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata'; +import { useImpactMetricsData } from 'hooks/api/getters/useImpactMetricsData/useImpactMetricsData'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { usePlaceholderData } from '../hooks/usePlaceholderData.js'; +import { ImpactMetricsControls } from './ImpactMetricsControls.tsx'; +import { getDateRange, getDisplayFormat, getTimeUnit } from './time-utils.ts'; + +type ImpactMetricsProps = {}; + +export const ImpactMetrics: FC = () => { + const theme = useTheme(); + const [selectedSeries, setSelectedSeries] = useState(''); + const [selectedRange, setSelectedRange] = useState< + 'day' | 'week' | 'month' + >('day'); + const [beginAtZero, setBeginAtZero] = useState(false); + + const { + metadata, + loading: metadataLoading, + error: metadataError, + } = useImpactMetricsMetadata(); + const { + data: timeSeriesData, + loading: dataLoading, + error: dataError, + } = useImpactMetricsData( + selectedSeries + ? { series: selectedSeries, range: selectedRange } + : undefined, + ); + + const placeholderData = usePlaceholderData({ + fill: true, + type: 'constant', + }); + + const data = useMemo(() => { + if (!timeSeriesData.length) { + return { + labels: [], + datasets: [ + { + data: [], + borderColor: theme.palette.primary.main, + backgroundColor: theme.palette.primary.light, + }, + ], + }; + } + + const timestamps = timeSeriesData.map( + ([epochTimestamp]) => new Date(epochTimestamp * 1000), + ); + const values = timeSeriesData.map(([, value]) => value); + + return { + labels: timestamps, + datasets: [ + { + data: values, + borderColor: theme.palette.primary.main, + backgroundColor: theme.palette.primary.light, + }, + ], + }; + }, [timeSeriesData, theme]); + + const hasError = metadataError || dataError; + const isLoading = metadataLoading || dataLoading; + const shouldShowPlaceholder = !selectedSeries || isLoading || hasError; + const notEnoughData = useMemo( + () => !isLoading && !data.datasets.some((d) => d.data.length > 1), + [data, isLoading], + ); + + const { min: minTime, max: maxTime } = getDateRange(selectedRange); + + const placeholder = selectedSeries ? ( + + ) : ( + + ); + const cover = notEnoughData ? placeholder : isLoading; + + return ( + + + + + + Failed to load impact metrics. Please check + if Prometheus is configured and the feature + flag is enabled. + + } + /> + + + + + Select a metric series to view the chart + + } + /> + + + + + + + + + ); +}; diff --git a/frontend/src/component/insights/impact-metrics/ImpactMetricsControls.tsx b/frontend/src/component/insights/impact-metrics/ImpactMetricsControls.tsx new file mode 100644 index 0000000000..1829f63b62 --- /dev/null +++ b/frontend/src/component/insights/impact-metrics/ImpactMetricsControls.tsx @@ -0,0 +1,94 @@ +import type { FC } from 'react'; +import { + FormControl, + InputLabel, + Select, + MenuItem, + FormControlLabel, + Checkbox, + Box, + Autocomplete, + TextField, + Typography, +} from '@mui/material'; + +export interface ImpactMetricsControlsProps { + selectedSeries: string; + onSeriesChange: (series: string) => void; + selectedRange: 'day' | 'week' | 'month'; + onRangeChange: (range: 'day' | 'week' | 'month') => void; + beginAtZero: boolean; + onBeginAtZeroChange: (beginAtZero: boolean) => void; + metricSeries: string[]; + loading?: boolean; +} + +export const ImpactMetricsControls: FC = ({ + selectedSeries, + onSeriesChange, + selectedRange, + onRangeChange, + beginAtZero, + onBeginAtZeroChange, + metricSeries, + loading = false, +}) => ( + ({ + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(3), + maxWidth: 400, + })} + > + + Select a custom metric to see its value over time. This can help you + understand the impact of your feature rollout on key outcomes, such + as system performance, usage patterns or error rates. + + + onSeriesChange(newValue || '')} + disabled={loading} + renderInput={(params) => ( + + )} + noOptionsText='No metrics available' + sx={{ minWidth: 300 }} + /> + + + Time + + + + onBeginAtZeroChange(e.target.checked)} + /> + } + label='Begin at zero' + /> + +); diff --git a/frontend/src/component/insights/impact-metrics/time-utils.ts b/frontend/src/component/insights/impact-metrics/time-utils.ts new file mode 100644 index 0000000000..1cceb0f5d5 --- /dev/null +++ b/frontend/src/component/insights/impact-metrics/time-utils.ts @@ -0,0 +1,57 @@ +export const getTimeUnit = (selectedRange: string) => { + switch (selectedRange) { + case 'day': + return 'hour'; + case 'week': + return 'day'; + case 'month': + return 'day'; + default: + return 'hour'; + } +}; + +export const getDisplayFormat = (selectedRange: string) => { + // TODO: localized format + switch (selectedRange) { + case 'day': + return 'MMM dd HH:mm'; + case 'week': + return 'MMM dd'; + case 'month': + return 'MMM dd'; + default: + return 'MMM dd HH:mm'; + } +}; + +export const getDateRange = (selectedRange: 'day' | 'week' | 'month') => { + const now = new Date(); + const endTime = now; + + switch (selectedRange) { + case 'day': { + const startTime = new Date(now); + startTime.setHours(now.getHours() - 24, 0, 0, 0); + return { min: startTime, max: endTime }; + } + case 'week': { + const startTime = new Date(now); + startTime.setDate(now.getDate() - 7); + startTime.setHours(0, 0, 0, 0); + const endTimeWeek = new Date(now); + endTimeWeek.setHours(23, 59, 59, 999); + return { min: startTime, max: endTimeWeek }; + } + case 'month': { + const startTime = new Date(now); + startTime.setDate(now.getDate() - 30); + startTime.setHours(0, 0, 0, 0); + const endTimeMonth = new Date(now); + endTimeMonth.setHours(23, 59, 59, 999); + return { min: startTime, max: endTimeMonth }; + } + default: + return { min: undefined, max: undefined }; + } +}; diff --git a/frontend/src/hooks/api/getters/useImpactMetricsData/useImpactMetricsData.ts b/frontend/src/hooks/api/getters/useImpactMetricsData/useImpactMetricsData.ts new file mode 100644 index 0000000000..888d48c7d2 --- /dev/null +++ b/frontend/src/hooks/api/getters/useImpactMetricsData/useImpactMetricsData.ts @@ -0,0 +1,42 @@ +import { fetcher, useApiGetter } from '../useApiGetter/useApiGetter.js'; +import { formatApiPath } from 'utils/formatPath'; + +export type TimeSeriesData = [number, number][]; + +export type ImpactMetricsQuery = { + series: string; + range: 'day' | 'week' | 'month'; +}; + +export const useImpactMetricsData = (query?: ImpactMetricsQuery) => { + const shouldFetch = Boolean(query?.series && query?.range); + + const createPath = () => { + if (!query) return ''; + const params = new URLSearchParams({ + series: query.series, + range: query.range, + }); + return `api/admin/impact-metrics/?${params.toString()}`; + }; + + const PATH = createPath(); + + const { data, refetch, loading, error } = useApiGetter( + shouldFetch ? formatApiPath(PATH) : null, + shouldFetch + ? () => fetcher(formatApiPath(PATH), 'Impact metrics data') + : () => Promise.resolve([]), + { + refreshInterval: 30 * 1_000, + revalidateOnFocus: true, + }, + ); + + return { + data: data || [], + refetch, + loading: shouldFetch ? loading : false, + error, + }; +}; diff --git a/frontend/src/hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata.ts b/frontend/src/hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata.ts new file mode 100644 index 0000000000..f0aaf003ed --- /dev/null +++ b/frontend/src/hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata.ts @@ -0,0 +1,22 @@ +import { fetcher, useApiGetter } from '../useApiGetter/useApiGetter.js'; +import { formatApiPath } from 'utils/formatPath'; + +export type ImpactMetricsMetadata = { + series: string[]; + labels: string[]; +}; + +export const useImpactMetricsMetadata = () => { + const PATH = `api/admin/impact-metrics/metadata`; + const { data, refetch, loading, error } = + useApiGetter(formatApiPath(PATH), () => + fetcher(formatApiPath(PATH), 'Impact metrics metadata'), + ); + + return { + metadata: data || { series: [], labels: [] }, + refetch, + loading, + error, + }; +}; diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts index a96e647889..fc4bc59793 100644 --- a/frontend/src/interfaces/uiConfig.ts +++ b/frontend/src/interfaces/uiConfig.ts @@ -91,6 +91,7 @@ export type UiFlags = { createFlagDialogCache?: boolean; healthToTechDebt?: boolean; improvedJsonDiff?: boolean; + impactMetrics?: boolean; }; export interface IVersionInfo { From 79dc5b652127b075ecd1da4107438114a28b3bc6 Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Wed, 18 Jun 2025 23:37:31 +0200 Subject: [PATCH 04/13] remove impact metrics backend --- .../createImpactMetricsService.ts | 10 -- .../impact-metrics-controller.ts | 67 -------- .../impact-metrics/impact-metrics-service.ts | 155 ------------------ src/lib/openapi/spec/impact-metrics-schema.ts | 15 -- src/lib/openapi/spec/index.ts | 1 - src/lib/services/index.ts | 11 -- 6 files changed, 259 deletions(-) delete mode 100644 src/lib/features/impact-metrics/createImpactMetricsService.ts delete mode 100644 src/lib/features/impact-metrics/impact-metrics-controller.ts delete mode 100644 src/lib/features/impact-metrics/impact-metrics-service.ts delete mode 100644 src/lib/openapi/spec/impact-metrics-schema.ts diff --git a/src/lib/features/impact-metrics/createImpactMetricsService.ts b/src/lib/features/impact-metrics/createImpactMetricsService.ts deleted file mode 100644 index 457a0b79b9..0000000000 --- a/src/lib/features/impact-metrics/createImpactMetricsService.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { IUnleashConfig } from '../../types/index.js'; -import ImpactMetricsService from './impact-metrics-service.js'; - -export const createImpactMetricsService = (config: IUnleashConfig) => { - return new ImpactMetricsService(config); -}; - -export const createFakeImpactMetricsService = (config: IUnleashConfig) => { - return new ImpactMetricsService(config); -}; diff --git a/src/lib/features/impact-metrics/impact-metrics-controller.ts b/src/lib/features/impact-metrics/impact-metrics-controller.ts deleted file mode 100644 index bc416ec703..0000000000 --- a/src/lib/features/impact-metrics/impact-metrics-controller.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { Response } from 'express'; -import Controller from '../../routes/controller.js'; -import type { IAuthRequest } from '../../routes/unleash-types.js'; -import type { IUnleashConfig } from '../../types/option.js'; -import { NONE } from '../../types/permissions.js'; -import type { OpenApiService } from '../../services/openapi-service.js'; -import { getStandardResponses } from '../../openapi/util/standard-responses.js'; -import { createResponseSchema } from '../../openapi/util/create-response-schema.js'; -import type ImpactMetricsService from './impact-metrics-service.js'; -import { - impactMetricsSchema, - type ImpactMetricsSchema, -} from '../../openapi/spec/impact-metrics-schema.js'; - -interface ImpactMetricsServices { - impactMetricsService: ImpactMetricsService; - openApiService: OpenApiService; -} - -export default class ImpactMetricsController extends Controller { - private impactMetricsService: ImpactMetricsService; - private openApiService: OpenApiService; - - constructor( - config: IUnleashConfig, - { impactMetricsService, openApiService }: ImpactMetricsServices, - ) { - super(config); - this.impactMetricsService = impactMetricsService; - this.openApiService = openApiService; - - this.route({ - method: 'get', - path: '', - handler: this.getImpactMetrics, - permission: NONE, - middleware: [ - openApiService.validPath({ - tags: ['Unstable'], - operationId: 'getImpactMetrics', - summary: 'Get impact metrics', - description: - 'Retrieves impact metrics data from Prometheus and forwards it to the frontend.', - responses: { - 200: createResponseSchema('impactMetricsSchema'), - ...getStandardResponses(401, 403, 404), - }, - }), - ], - }); - } - - async getImpactMetrics( - req: IAuthRequest, - res: Response, - ): Promise { - const metrics = - await this.impactMetricsService.getMetricsFromPrometheus(); - - this.openApiService.respondWithValidation( - 200, - res, - impactMetricsSchema.$id, - metrics, - ); - } -} diff --git a/src/lib/features/impact-metrics/impact-metrics-service.ts b/src/lib/features/impact-metrics/impact-metrics-service.ts deleted file mode 100644 index fa57886558..0000000000 --- a/src/lib/features/impact-metrics/impact-metrics-service.ts +++ /dev/null @@ -1,155 +0,0 @@ -import type { Logger } from '../../logger.js'; -import type { IUnleashConfig } from '../../types/index.js'; - -interface PrometheusResponse { - status: string; - data: any; -} - -interface MetricSeries { - name: string; - available: boolean; - labels: Record; -} - -export interface IImpactMetricsService { - getMetricsFromPrometheus(): Promise<{ - availableMetrics: MetricSeries[]; - totalCount: number; - }>; -} - -// TODO: URL -const prometheusUrl = 'http://localhost:9090'; - -export default class ImpactMetricsService implements IImpactMetricsService { - private logger: Logger; - private config: IUnleashConfig; - - constructor(config: IUnleashConfig) { - this.logger = config.getLogger( - 'impact-metrics/impact-metrics-service.ts', - ); - this.config = config; - } - - async getMetricsFromPrometheus(): Promise<{ - availableMetrics: MetricSeries[]; - totalCount: number; - }> { - const testingMetrics = [ - 'node_cpu_seconds_total', - 'node_load5', - 'node_memory_MemAvailable_bytes', - 'node_disk_read_bytes_total', - 'node_disk_written_bytes_total', - 'node_network_receive_bytes_total', - 'node_network_transmit_bytes_total', - 'node_filesystem_avail_bytes', - 'node_filesystem_size_bytes', - ]; - - try { - this.logger.info('Fetching all metrics from Prometheus'); - - const metricsResponse = await fetch( - `${prometheusUrl}/api/v1/label/__name__/values`, - ); - - if (!metricsResponse.ok) { - throw new Error( - `Failed to fetch metrics: ${metricsResponse.status}`, - ); - } - - const metricsData = - (await metricsResponse.json()) as PrometheusResponse; - if (metricsData.status !== 'success') { - throw new Error('Prometheus API returned error status'); - } - - const allMetrics: string[] = metricsData.data; - this.logger.info( - `Found ${allMetrics.length} total metrics in Prometheus`, - ); - - const availableMetrics: MetricSeries[] = []; - - for (const metricName of testingMetrics) { - const isAvailable = allMetrics.includes(metricName); - - let labels: Record = {}; - - if (isAvailable) { - try { - const seriesUrl = `${prometheusUrl}/api/v1/series?match[]=${encodeURIComponent(metricName)}`; - this.logger.debug( - `Fetching series for metric: ${metricName}`, - ); - - const seriesResponse = await fetch(seriesUrl); - const seriesData = - (await seriesResponse.json()) as PrometheusResponse; - if (seriesData.status === 'success') { - const labelMap: Record> = {}; - - seriesData.data.forEach( - (series: Record) => { - Object.entries(series).forEach( - ([key, value]) => { - if (key !== '__name__') { - if (!labelMap[key]) { - labelMap[key] = new Set(); - } - labelMap[key].add(value); - } - }, - ); - }, - ); - - labels = Object.fromEntries( - Object.entries(labelMap).map( - ([key, valueSet]) => [ - key, - Array.from(valueSet).sort(), - ], - ), - ); - } - } catch (error) { - this.logger.warn( - `Failed to fetch labels for metric ${metricName}:`, - error, - ); - } - } - - availableMetrics.push({ - name: metricName, - available: isAvailable, - labels, - }); - - this.logger.debug( - `Metric ${metricName}: available=${isAvailable}, labelCount=${Object.keys(labels).length}`, - ); - } - - const availableCount = availableMetrics.filter( - (m) => m.available, - ).length; - this.logger.info( - `Found ${availableCount}/${testingMetrics.length} test metrics available in Prometheus`, - ); - - return { - availableMetrics, - totalCount: allMetrics.length, - }; - } catch (error) { - this.logger.error('Error fetching metrics from Prometheus:', error); - throw error; - } - } -} diff --git a/src/lib/openapi/spec/impact-metrics-schema.ts b/src/lib/openapi/spec/impact-metrics-schema.ts deleted file mode 100644 index 6b877dfec1..0000000000 --- a/src/lib/openapi/spec/impact-metrics-schema.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { FromSchema } from 'json-schema-to-ts'; - -export const impactMetricsSchema = { - $id: '#/components/schemas/impactMetricsSchema', - type: 'object', - description: 'Impact metrics data from Prometheus', - additionalProperties: true, - required: [], - properties: {}, - components: { - schemas: {}, - }, -} as const; - -export type ImpactMetricsSchema = FromSchema; diff --git a/src/lib/openapi/spec/index.ts b/src/lib/openapi/spec/index.ts index 6e44c2ba4b..04771b2f0e 100644 --- a/src/lib/openapi/spec/index.ts +++ b/src/lib/openapi/spec/index.ts @@ -113,7 +113,6 @@ export * from './health-overview-schema.js'; export * from './health-report-schema.js'; export * from './id-schema.js'; export * from './ids-schema.js'; -export * from './impact-metrics-schema.js'; export * from './import-toggles-schema.js'; export * from './import-toggles-validate-item-schema.js'; export * from './import-toggles-validate-schema.js'; diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index 19aaf35332..0f80b77f38 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -171,11 +171,6 @@ import type { import type { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType.js'; import { UnknownFlagsService } from '../features/metrics/unknown-flags/unknown-flags-service.js'; import type FeatureLinkService from '../features/feature-links/feature-link-service.js'; -import { - createFakeImpactMetricsService, - createImpactMetricsService, -} from '../features/impact-metrics/createImpactMetricsService.js'; -import type ImpactMetricsService from '../features/impact-metrics/impact-metrics-service.js'; export const createServices = ( stores: IUnleashStores, @@ -446,10 +441,6 @@ export const createServices = ( ? withTransactional(createUserSubscriptionsService(config), db) : withFakeTransactional(createFakeUserSubscriptionsService(config)); - const impactMetricsService = db - ? createImpactMetricsService(config) - : createFakeImpactMetricsService(config); - return { transactionalAccessService, accessService, @@ -516,7 +507,6 @@ export const createServices = ( integrationEventsService, onboardingService, personalDashboardService, - impactMetricsService, projectStatusService, transactionalUserSubscriptionsService, uniqueConnectionService, @@ -648,7 +638,6 @@ export interface IUnleashServices { integrationEventsService: IntegrationEventsService; onboardingService: OnboardingService; personalDashboardService: PersonalDashboardService; - impactMetricsService: ImpactMetricsService; projectStatusService: ProjectStatusService; transactionalUserSubscriptionsService: WithTransactional; uniqueConnectionService: UniqueConnectionService; From 7ac1d0386a5e462bbb61cb70c9b443fedc3c0211 Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Wed, 18 Jun 2025 23:52:10 +0200 Subject: [PATCH 05/13] add 'hour' range --- .../insights/impact-metrics/ImpactMetrics.tsx | 37 +++++++------------ .../impact-metrics/ImpactMetricsControls.tsx | 9 +++-- .../insights/impact-metrics/time-utils.ts | 15 +++++++- .../useImpactMetricsData.ts | 2 +- 4 files changed, 33 insertions(+), 30 deletions(-) diff --git a/frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx b/frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx index bf819e5162..bd4c4082a3 100644 --- a/frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx +++ b/frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx @@ -13,7 +13,6 @@ import { } from 'component/insights/InsightsCharts.styles'; import { useImpactMetricsMetadata } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata'; import { useImpactMetricsData } from 'hooks/api/getters/useImpactMetricsData/useImpactMetricsData'; -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { usePlaceholderData } from '../hooks/usePlaceholderData.js'; import { ImpactMetricsControls } from './ImpactMetricsControls.tsx'; import { getDateRange, getDisplayFormat, getTimeUnit } from './time-utils.ts'; @@ -24,7 +23,7 @@ export const ImpactMetrics: FC = () => { const theme = useTheme(); const [selectedSeries, setSelectedSeries] = useState(''); const [selectedRange, setSelectedRange] = useState< - 'day' | 'week' | 'month' + 'hour' | 'day' | 'week' | 'month' >('day'); const [beginAtZero, setBeginAtZero] = useState(false); @@ -111,17 +110,6 @@ export const ImpactMetrics: FC = () => { width: '100%', }} > - - Failed to load impact metrics. Please check - if Prometheus is configured and the feature - flag is enabled. - - } - /> - = () => { loading={metadataLoading} /> - - Select a metric series to view the chart - - } - /> + {!selectedSeries && !isLoading ? ( + + Select a metric series to view the chart + + ) : null} + {hasError ? ( + + Failed to load impact metrics. Please check if + Prometheus is configured and the feature flag is + enabled. + + ) : null} void; - selectedRange: 'day' | 'week' | 'month'; - onRangeChange: (range: 'day' | 'week' | 'month') => void; + selectedRange: 'hour' | 'day' | 'week' | 'month'; + onRangeChange: (range: 'hour' | 'day' | 'week' | 'month') => void; beginAtZero: boolean; onBeginAtZeroChange: (beginAtZero: boolean) => void; metricSeries: string[]; @@ -71,10 +71,13 @@ export const ImpactMetricsControls: FC = ({ labelId='range-select-label' value={selectedRange} onChange={(e) => - onRangeChange(e.target.value as 'day' | 'week' | 'month') + onRangeChange( + e.target.value as 'hour' | 'day' | 'week' | 'month', + ) } label='Time Range' > + Last hour Last 24 hours Last 7 days Last 30 days diff --git a/frontend/src/component/insights/impact-metrics/time-utils.ts b/frontend/src/component/insights/impact-metrics/time-utils.ts index 1cceb0f5d5..2f56ede49d 100644 --- a/frontend/src/component/insights/impact-metrics/time-utils.ts +++ b/frontend/src/component/insights/impact-metrics/time-utils.ts @@ -1,5 +1,7 @@ export const getTimeUnit = (selectedRange: string) => { switch (selectedRange) { + case 'hour': + return 'minute'; case 'day': return 'hour'; case 'week': @@ -14,8 +16,10 @@ export const getTimeUnit = (selectedRange: string) => { export const getDisplayFormat = (selectedRange: string) => { // TODO: localized format switch (selectedRange) { + case 'hour': + return 'HH:mm:ss'; case 'day': - return 'MMM dd HH:mm'; + return 'HH:mm'; case 'week': return 'MMM dd'; case 'month': @@ -25,11 +29,18 @@ export const getDisplayFormat = (selectedRange: string) => { } }; -export const getDateRange = (selectedRange: 'day' | 'week' | 'month') => { +export const getDateRange = ( + selectedRange: 'hour' | 'day' | 'week' | 'month', +) => { const now = new Date(); const endTime = now; switch (selectedRange) { + case 'hour': { + const startTime = new Date(now); + startTime.setMinutes(now.getMinutes() - 60, 0, 0); + return { min: startTime, max: endTime }; + } case 'day': { const startTime = new Date(now); startTime.setHours(now.getHours() - 24, 0, 0, 0); diff --git a/frontend/src/hooks/api/getters/useImpactMetricsData/useImpactMetricsData.ts b/frontend/src/hooks/api/getters/useImpactMetricsData/useImpactMetricsData.ts index 888d48c7d2..aac5a838fc 100644 --- a/frontend/src/hooks/api/getters/useImpactMetricsData/useImpactMetricsData.ts +++ b/frontend/src/hooks/api/getters/useImpactMetricsData/useImpactMetricsData.ts @@ -5,7 +5,7 @@ export type TimeSeriesData = [number, number][]; export type ImpactMetricsQuery = { series: string; - range: 'day' | 'week' | 'month'; + range: 'hour' | 'day' | 'week' | 'month'; }; export const useImpactMetricsData = (query?: ImpactMetricsQuery) => { From 2fec56a6a3b7ef3ef288ab8629208f2ed1ca9332 Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Thu, 19 Jun 2025 10:59:14 +0200 Subject: [PATCH 06/13] update: theme --- .../src/component/insights/impact-metrics/ImpactMetrics.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx b/frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx index bd4c4082a3..776d3e37a8 100644 --- a/frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx +++ b/frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx @@ -103,12 +103,12 @@ export const ImpactMetrics: FC = () => { ({ display: 'flex', flexDirection: 'column', - gap: 2, + gap: theme.spacing(2), width: '100%', - }} + })} > Date: Thu, 19 Jun 2025 15:25:42 +0200 Subject: [PATCH 07/13] remove placeholder data --- frontend/src/component/insights/data.ts | 622 ------------------------ 1 file changed, 622 deletions(-) delete mode 100644 frontend/src/component/insights/data.ts diff --git a/frontend/src/component/insights/data.ts b/frontend/src/component/insights/data.ts deleted file mode 100644 index 1b34538233..0000000000 --- a/frontend/src/component/insights/data.ts +++ /dev/null @@ -1,622 +0,0 @@ -export const data = [ - { - schema: { - refId: 'A', - meta: { - type: 'timeseries-multi', - typeVersion: [0, 0], - custom: { - resultType: 'matrix', - }, - executedQueryString: - 'Expr: sum(topk(1, instance_users{state="ACTIVE", plan=~"Pro|Enterprise"}) by(clientId))\nStep: 30m0s', - preferredVisualisationType: 'graph', - }, - name: 'sum(topk(1, instance_users{state="ACTIVE", plan=~"Pro|Enterprise"}) by(clientId))', - fields: [ - { - name: 'Time', - type: 'time', - typeInfo: { - frame: 'time.Time', - }, - config: { - interval: 1800000, - }, - }, - { - name: 'Value', - type: 'number', - typeInfo: { - frame: 'float64', - }, - labels: {}, - config: { - displayNameFromDS: - 'sum(topk(1, instance_users{state="ACTIVE", plan=~"Pro|Enterprise"}) by(clientId))', - }, - }, - ], - }, - data: { - values: [ - [ - 1747512000000, 1747513800000, 1747515600000, 1747517400000, - 1747519200000, 1747521000000, 1747522800000, 1747524600000, - 1747526400000, 1747528200000, 1747530000000, 1747531800000, - 1747533600000, 1747535400000, 1747537200000, 1747539000000, - 1747540800000, 1747542600000, 1747544400000, 1747546200000, - 1747548000000, 1747549800000, 1747551600000, 1747553400000, - 1747555200000, 1747557000000, 1747558800000, 1747560600000, - 1747562400000, 1747564200000, 1747566000000, 1747567800000, - 1747569600000, 1747571400000, 1747573200000, 1747575000000, - 1747576800000, 1747578600000, 1747580400000, 1747582200000, - 1747584000000, 1747585800000, 1747587600000, 1747589400000, - 1747591200000, 1747593000000, 1747594800000, 1747596600000, - 1747598400000, 1747600200000, 1747602000000, 1747603800000, - 1747605600000, 1747607400000, 1747609200000, 1747611000000, - 1747612800000, 1747614600000, 1747616400000, 1747618200000, - 1747620000000, 1747621800000, 1747623600000, 1747625400000, - 1747627200000, 1747629000000, 1747630800000, 1747632600000, - 1747634400000, 1747636200000, 1747638000000, 1747639800000, - 1747641600000, 1747643400000, 1747645200000, 1747647000000, - 1747648800000, 1747650600000, 1747652400000, 1747654200000, - 1747656000000, 1747657800000, 1747659600000, 1747661400000, - 1747663200000, 1747665000000, 1747666800000, 1747668600000, - 1747670400000, 1747672200000, 1747674000000, 1747675800000, - 1747677600000, 1747679400000, 1747681200000, 1747683000000, - 1747684800000, 1747686600000, 1747688400000, 1747690200000, - 1747692000000, 1747693800000, 1747695600000, 1747697400000, - 1747699200000, 1747701000000, 1747702800000, 1747704600000, - 1747706400000, 1747708200000, 1747710000000, 1747711800000, - 1747713600000, 1747715400000, 1747717200000, 1747719000000, - 1747720800000, 1747722600000, 1747724400000, 1747726200000, - 1747728000000, 1747729800000, 1747731600000, 1747733400000, - 1747735200000, 1747737000000, 1747738800000, 1747740600000, - 1747742400000, 1747744200000, 1747746000000, 1747747800000, - 1747749600000, 1747751400000, 1747753200000, 1747755000000, - 1747756800000, 1747758600000, 1747760400000, 1747762200000, - 1747764000000, 1747765800000, 1747767600000, 1747769400000, - 1747771200000, 1747773000000, 1747774800000, 1747776600000, - 1747778400000, 1747780200000, 1747782000000, 1747783800000, - 1747785600000, 1747787400000, 1747789200000, 1747791000000, - 1747792800000, 1747794600000, 1747796400000, 1747798200000, - 1747800000000, 1747801800000, 1747803600000, 1747805400000, - 1747807200000, 1747809000000, 1747810800000, 1747812600000, - 1747814400000, 1747816200000, 1747818000000, 1747819800000, - 1747821600000, 1747823400000, 1747825200000, 1747827000000, - 1747828800000, 1747830600000, 1747832400000, 1747834200000, - 1747836000000, 1747837800000, 1747839600000, 1747841400000, - 1747843200000, 1747845000000, 1747846800000, 1747848600000, - 1747850400000, 1747852200000, 1747854000000, 1747855800000, - 1747857600000, 1747859400000, 1747861200000, 1747863000000, - 1747864800000, 1747866600000, 1747868400000, 1747870200000, - 1747872000000, 1747873800000, 1747875600000, 1747877400000, - 1747879200000, 1747881000000, 1747882800000, 1747884600000, - 1747886400000, 1747888200000, 1747890000000, 1747891800000, - 1747893600000, 1747895400000, 1747897200000, 1747899000000, - 1747900800000, 1747902600000, 1747904400000, 1747906200000, - 1747908000000, 1747909800000, 1747911600000, 1747913400000, - 1747915200000, 1747917000000, 1747918800000, 1747920600000, - 1747922400000, 1747924200000, 1747926000000, 1747927800000, - 1747929600000, 1747931400000, 1747933200000, 1747935000000, - 1747936800000, 1747938600000, 1747940400000, 1747942200000, - 1747944000000, 1747945800000, 1747947600000, 1747949400000, - 1747951200000, 1747953000000, 1747954800000, 1747956600000, - 1747958400000, 1747960200000, 1747962000000, 1747963800000, - 1747965600000, 1747967400000, 1747969200000, 1747971000000, - 1747972800000, 1747974600000, 1747976400000, 1747978200000, - 1747980000000, 1747981800000, 1747983600000, 1747985400000, - 1747987200000, 1747989000000, 1747990800000, 1747992600000, - 1747994400000, 1747996200000, 1747998000000, 1747999800000, - 1748001600000, 1748003400000, 1748005200000, 1748007000000, - 1748008800000, 1748010600000, 1748012400000, 1748014200000, - 1748016000000, 1748017800000, 1748019600000, 1748021400000, - 1748023200000, 1748025000000, 1748026800000, 1748028600000, - 1748030400000, 1748032200000, 1748034000000, 1748035800000, - 1748037600000, 1748039400000, 1748041200000, 1748043000000, - 1748044800000, 1748046600000, 1748048400000, 1748050200000, - 1748052000000, 1748053800000, 1748055600000, 1748057400000, - 1748059200000, 1748061000000, 1748062800000, 1748064600000, - 1748066400000, 1748068200000, 1748070000000, 1748071800000, - 1748073600000, 1748075400000, 1748077200000, 1748079000000, - 1748080800000, 1748082600000, 1748084400000, 1748086200000, - 1748088000000, 1748089800000, 1748091600000, 1748093400000, - 1748095200000, 1748097000000, 1748098800000, 1748100600000, - 1748102400000, 1748104200000, 1748106000000, 1748107800000, - 1748109600000, 1748111400000, 1748113200000, 1748115000000, - 1748116800000, 1748118600000, 1748120400000, 1748122200000, - 1748124000000, 1748125800000, 1748127600000, 1748129400000, - 1748131200000, 1748133000000, 1748134800000, 1748136600000, - 1748138400000, 1748140200000, 1748142000000, 1748143800000, - 1748145600000, 1748147400000, 1748149200000, 1748151000000, - 1748152800000, 1748154600000, 1748156400000, 1748158200000, - 1748160000000, 1748161800000, 1748163600000, 1748165400000, - 1748167200000, 1748169000000, 1748170800000, 1748172600000, - 1748174400000, 1748176200000, 1748178000000, 1748179800000, - 1748181600000, 1748183400000, 1748185200000, 1748187000000, - 1748188800000, 1748190600000, 1748192400000, 1748194200000, - 1748196000000, 1748197800000, 1748199600000, 1748201400000, - 1748203200000, 1748205000000, 1748206800000, 1748208600000, - 1748210400000, 1748212200000, 1748214000000, 1748215800000, - 1748217600000, 1748219400000, 1748221200000, 1748223000000, - 1748224800000, 1748226600000, 1748228400000, 1748230200000, - 1748232000000, 1748233800000, 1748235600000, 1748237400000, - 1748239200000, 1748241000000, 1748242800000, 1748244600000, - 1748246400000, 1748248200000, 1748250000000, 1748251800000, - 1748253600000, 1748255400000, 1748257200000, 1748259000000, - 1748260800000, 1748262600000, 1748264400000, 1748266200000, - 1748268000000, 1748269800000, 1748271600000, 1748273400000, - 1748275200000, 1748277000000, 1748278800000, 1748280600000, - 1748282400000, 1748284200000, 1748286000000, 1748287800000, - 1748289600000, 1748291400000, 1748293200000, 1748295000000, - 1748296800000, 1748298600000, 1748300400000, 1748302200000, - 1748304000000, 1748305800000, 1748307600000, 1748309400000, - 1748311200000, 1748313000000, 1748314800000, 1748316600000, - 1748318400000, 1748320200000, 1748322000000, 1748323800000, - 1748325600000, 1748327400000, 1748329200000, 1748331000000, - 1748332800000, 1748334600000, 1748336400000, 1748338200000, - 1748340000000, 1748341800000, 1748343600000, 1748345400000, - 1748347200000, 1748349000000, 1748350800000, 1748352600000, - 1748354400000, 1748356200000, 1748358000000, 1748359800000, - 1748361600000, 1748363400000, 1748365200000, 1748367000000, - 1748368800000, 1748370600000, 1748372400000, 1748374200000, - 1748376000000, 1748377800000, 1748379600000, 1748381400000, - 1748383200000, 1748385000000, 1748386800000, 1748388600000, - 1748390400000, 1748392200000, 1748394000000, 1748395800000, - 1748397600000, 1748399400000, 1748401200000, 1748403000000, - 1748404800000, 1748406600000, 1748408400000, 1748410200000, - 1748412000000, 1748413800000, 1748415600000, 1748417400000, - 1748419200000, 1748421000000, 1748422800000, 1748424600000, - 1748426400000, 1748428200000, 1748430000000, 1748431800000, - 1748433600000, 1748435400000, 1748437200000, 1748439000000, - 1748440800000, 1748442600000, 1748444400000, 1748446200000, - 1748448000000, 1748449800000, 1748451600000, 1748453400000, - 1748455200000, 1748457000000, 1748458800000, 1748460600000, - 1748462400000, 1748464200000, 1748466000000, 1748467800000, - 1748469600000, 1748471400000, 1748473200000, 1748475000000, - 1748476800000, 1748478600000, 1748480400000, 1748482200000, - 1748484000000, 1748485800000, 1748487600000, 1748489400000, - 1748491200000, 1748493000000, 1748494800000, 1748496600000, - 1748498400000, 1748500200000, 1748502000000, 1748503800000, - 1748505600000, 1748507400000, 1748509200000, 1748511000000, - 1748512800000, 1748514600000, 1748516400000, 1748518200000, - 1748520000000, 1748521800000, 1748523600000, 1748525400000, - 1748527200000, 1748529000000, 1748530800000, 1748532600000, - 1748534400000, 1748536200000, 1748538000000, 1748539800000, - 1748541600000, 1748543400000, 1748545200000, 1748547000000, - 1748548800000, 1748550600000, 1748552400000, 1748554200000, - 1748556000000, 1748557800000, 1748559600000, 1748561400000, - 1748563200000, 1748565000000, 1748566800000, 1748568600000, - 1748570400000, 1748572200000, 1748574000000, 1748575800000, - 1748577600000, 1748579400000, 1748581200000, 1748583000000, - 1748584800000, 1748586600000, 1748588400000, 1748590200000, - 1748592000000, 1748593800000, 1748595600000, 1748597400000, - 1748599200000, 1748601000000, 1748602800000, 1748604600000, - 1748606400000, 1748608200000, 1748610000000, 1748611800000, - 1748613600000, 1748615400000, 1748617200000, 1748619000000, - 1748620800000, 1748622600000, 1748624400000, 1748626200000, - 1748628000000, 1748629800000, 1748631600000, 1748633400000, - 1748635200000, 1748637000000, 1748638800000, 1748640600000, - 1748642400000, 1748644200000, 1748646000000, 1748647800000, - 1748649600000, 1748651400000, 1748653200000, 1748655000000, - 1748656800000, 1748658600000, 1748660400000, 1748662200000, - 1748664000000, 1748665800000, 1748667600000, 1748669400000, - 1748671200000, 1748673000000, 1748674800000, 1748676600000, - 1748678400000, 1748680200000, 1748682000000, 1748683800000, - 1748685600000, 1748687400000, 1748689200000, 1748691000000, - 1748692800000, 1748694600000, 1748696400000, 1748698200000, - 1748700000000, 1748701800000, 1748703600000, 1748705400000, - 1748707200000, 1748709000000, 1748710800000, 1748712600000, - 1748714400000, 1748716200000, 1748718000000, 1748719800000, - 1748721600000, 1748723400000, 1748725200000, 1748727000000, - 1748728800000, 1748730600000, 1748732400000, 1748734200000, - 1748736000000, 1748737800000, 1748739600000, 1748741400000, - 1748743200000, 1748745000000, 1748746800000, 1748748600000, - 1748750400000, 1748752200000, 1748754000000, 1748755800000, - 1748757600000, 1748759400000, 1748761200000, 1748763000000, - 1748764800000, 1748766600000, 1748768400000, 1748770200000, - 1748772000000, 1748773800000, 1748775600000, 1748777400000, - 1748779200000, 1748781000000, 1748782800000, 1748784600000, - 1748786400000, 1748788200000, 1748790000000, 1748791800000, - 1748793600000, 1748795400000, 1748797200000, 1748799000000, - 1748800800000, 1748802600000, 1748804400000, 1748806200000, - 1748808000000, 1748809800000, 1748811600000, 1748813400000, - 1748815200000, 1748817000000, 1748818800000, 1748820600000, - 1748822400000, 1748824200000, 1748826000000, 1748827800000, - 1748829600000, 1748831400000, 1748833200000, 1748835000000, - 1748836800000, 1748838600000, 1748840400000, 1748842200000, - 1748844000000, 1748845800000, 1748847600000, 1748849400000, - 1748851200000, 1748853000000, 1748854800000, 1748856600000, - 1748858400000, 1748860200000, 1748862000000, 1748863800000, - 1748865600000, 1748867400000, 1748869200000, 1748871000000, - 1748872800000, 1748874600000, 1748876400000, 1748878200000, - 1748880000000, 1748881800000, 1748883600000, 1748885400000, - 1748887200000, 1748889000000, 1748890800000, 1748892600000, - 1748894400000, 1748896200000, 1748898000000, 1748899800000, - 1748901600000, 1748903400000, 1748905200000, 1748907000000, - 1748908800000, 1748910600000, 1748912400000, 1748914200000, - 1748916000000, 1748917800000, 1748919600000, 1748921400000, - 1748923200000, 1748925000000, 1748926800000, 1748928600000, - 1748930400000, 1748932200000, 1748934000000, 1748935800000, - 1748937600000, 1748939400000, 1748941200000, 1748943000000, - 1748944800000, 1748946600000, 1748948400000, 1748950200000, - 1748952000000, 1748953800000, 1748955600000, 1748957400000, - 1748959200000, 1748961000000, 1748962800000, 1748964600000, - 1748966400000, 1748968200000, 1748970000000, 1748971800000, - 1748973600000, 1748975400000, 1748977200000, 1748979000000, - 1748980800000, 1748982600000, 1748984400000, 1748986200000, - 1748988000000, 1748989800000, 1748991600000, 1748993400000, - 1748995200000, 1748997000000, 1748998800000, 1749000600000, - 1749002400000, 1749004200000, 1749006000000, 1749007800000, - 1749009600000, 1749011400000, 1749013200000, 1749015000000, - 1749016800000, 1749018600000, 1749020400000, 1749022200000, - 1749024000000, 1749025800000, 1749027600000, 1749029400000, - 1749031200000, 1749033000000, 1749034800000, 1749036600000, - 1749038400000, 1749040200000, 1749042000000, 1749043800000, - 1749045600000, 1749047400000, 1749049200000, 1749051000000, - 1749052800000, 1749054600000, 1749056400000, 1749058200000, - 1749060000000, 1749061800000, 1749063600000, 1749065400000, - 1749067200000, 1749069000000, 1749070800000, 1749072600000, - 1749074400000, 1749076200000, 1749078000000, 1749079800000, - 1749081600000, 1749083400000, 1749085200000, 1749087000000, - 1749088800000, 1749090600000, 1749092400000, 1749094200000, - 1749096000000, 1749097800000, 1749099600000, 1749101400000, - 1749103200000, 1749105000000, 1749106800000, 1749108600000, - 1749110400000, 1749112200000, 1749114000000, 1749115800000, - 1749117600000, 1749119400000, 1749121200000, 1749123000000, - 1749124800000, 1749126600000, 1749128400000, 1749130200000, - 1749132000000, 1749133800000, 1749135600000, 1749137400000, - 1749139200000, 1749141000000, 1749142800000, 1749144600000, - 1749146400000, 1749148200000, 1749150000000, 1749151800000, - 1749153600000, 1749155400000, 1749157200000, 1749159000000, - 1749160800000, 1749162600000, 1749164400000, 1749166200000, - 1749168000000, 1749169800000, 1749171600000, 1749173400000, - 1749175200000, 1749177000000, 1749178800000, 1749180600000, - 1749182400000, 1749184200000, 1749186000000, 1749187800000, - 1749189600000, 1749191400000, 1749193200000, 1749195000000, - 1749196800000, 1749198600000, 1749200400000, 1749202200000, - 1749204000000, 1749205800000, 1749207600000, 1749209400000, - 1749211200000, 1749213000000, 1749214800000, 1749216600000, - 1749218400000, 1749220200000, 1749222000000, 1749223800000, - 1749225600000, 1749227400000, 1749229200000, 1749231000000, - 1749232800000, 1749234600000, 1749236400000, 1749238200000, - 1749240000000, 1749241800000, 1749243600000, 1749245400000, - 1749247200000, 1749249000000, 1749250800000, 1749252600000, - 1749254400000, 1749256200000, 1749258000000, 1749259800000, - 1749261600000, 1749263400000, 1749265200000, 1749267000000, - 1749268800000, 1749270600000, 1749272400000, 1749274200000, - 1749276000000, 1749277800000, 1749279600000, 1749281400000, - 1749283200000, 1749285000000, 1749286800000, 1749288600000, - 1749290400000, 1749292200000, 1749294000000, 1749295800000, - 1749297600000, 1749299400000, 1749301200000, 1749303000000, - 1749304800000, 1749306600000, 1749308400000, 1749310200000, - 1749312000000, 1749313800000, 1749315600000, 1749317400000, - 1749319200000, 1749321000000, 1749322800000, 1749324600000, - 1749326400000, 1749328200000, 1749330000000, 1749331800000, - 1749333600000, 1749335400000, 1749337200000, 1749339000000, - 1749340800000, 1749342600000, 1749344400000, 1749346200000, - 1749348000000, 1749349800000, 1749351600000, 1749353400000, - 1749355200000, 1749357000000, 1749358800000, 1749360600000, - 1749362400000, 1749364200000, 1749366000000, 1749367800000, - 1749369600000, 1749371400000, 1749373200000, 1749375000000, - 1749376800000, 1749378600000, 1749380400000, 1749382200000, - 1749384000000, 1749385800000, 1749387600000, 1749389400000, - 1749391200000, 1749393000000, 1749394800000, 1749396600000, - 1749398400000, 1749400200000, 1749402000000, 1749403800000, - 1749405600000, 1749407400000, 1749409200000, 1749411000000, - 1749412800000, 1749414600000, 1749416400000, 1749418200000, - 1749420000000, 1749421800000, 1749423600000, 1749425400000, - 1749427200000, 1749429000000, 1749430800000, 1749432600000, - 1749434400000, 1749436200000, 1749438000000, 1749439800000, - 1749441600000, 1749443400000, 1749445200000, 1749447000000, - 1749448800000, 1749450600000, 1749452400000, 1749454200000, - 1749456000000, 1749457800000, 1749459600000, 1749461400000, - 1749463200000, 1749465000000, 1749466800000, 1749468600000, - 1749470400000, 1749472200000, 1749474000000, 1749475800000, - 1749477600000, 1749479400000, 1749481200000, 1749483000000, - 1749484800000, 1749486600000, 1749488400000, 1749490200000, - 1749492000000, 1749493800000, 1749495600000, 1749497400000, - 1749499200000, 1749501000000, 1749502800000, 1749504600000, - 1749506400000, 1749508200000, 1749510000000, 1749511800000, - 1749513600000, 1749515400000, 1749517200000, 1749519000000, - 1749520800000, 1749522600000, 1749524400000, 1749526200000, - 1749528000000, 1749529800000, 1749531600000, 1749533400000, - 1749535200000, 1749537000000, 1749538800000, 1749540600000, - 1749542400000, 1749544200000, 1749546000000, 1749547800000, - 1749549600000, 1749551400000, 1749553200000, 1749555000000, - 1749556800000, 1749558600000, 1749560400000, 1749562200000, - 1749564000000, 1749565800000, 1749567600000, 1749569400000, - 1749571200000, 1749573000000, 1749574800000, 1749576600000, - 1749578400000, 1749580200000, 1749582000000, 1749583800000, - 1749585600000, 1749587400000, 1749589200000, 1749591000000, - 1749592800000, 1749594600000, 1749596400000, 1749598200000, - 1749600000000, 1749601800000, 1749603600000, 1749605400000, - 1749607200000, 1749609000000, 1749610800000, 1749612600000, - 1749614400000, 1749616200000, 1749618000000, 1749619800000, - 1749621600000, 1749623400000, 1749625200000, 1749627000000, - 1749628800000, 1749630600000, 1749632400000, 1749634200000, - 1749636000000, 1749637800000, 1749639600000, 1749641400000, - 1749643200000, 1749645000000, 1749646800000, 1749648600000, - 1749650400000, 1749652200000, 1749654000000, 1749655800000, - 1749657600000, 1749659400000, 1749661200000, 1749663000000, - 1749664800000, 1749666600000, 1749668400000, 1749670200000, - 1749672000000, 1749673800000, 1749675600000, 1749677400000, - 1749679200000, 1749681000000, 1749682800000, 1749684600000, - 1749686400000, 1749688200000, 1749690000000, 1749691800000, - 1749693600000, 1749695400000, 1749697200000, 1749699000000, - 1749700800000, 1749702600000, 1749704400000, 1749706200000, - 1749708000000, 1749709800000, 1749711600000, 1749713400000, - 1749715200000, 1749717000000, 1749718800000, 1749720600000, - 1749722400000, 1749724200000, 1749726000000, 1749727800000, - 1749729600000, 1749731400000, 1749733200000, 1749735000000, - 1749736800000, 1749738600000, 1749740400000, 1749742200000, - 1749744000000, 1749745800000, 1749747600000, 1749749400000, - 1749751200000, 1749753000000, 1749754800000, 1749756600000, - 1749758400000, 1749760200000, 1749762000000, 1749763800000, - 1749765600000, 1749767400000, 1749769200000, 1749771000000, - 1749772800000, 1749774600000, 1749776400000, 1749778200000, - 1749780000000, 1749781800000, 1749783600000, 1749785400000, - 1749787200000, 1749789000000, 1749790800000, 1749792600000, - 1749794400000, 1749796200000, 1749798000000, 1749799800000, - 1749801600000, 1749803400000, 1749805200000, 1749807000000, - 1749808800000, 1749810600000, 1749812400000, 1749814200000, - 1749816000000, 1749817800000, 1749819600000, 1749821400000, - 1749823200000, 1749825000000, 1749826800000, 1749828600000, - 1749830400000, 1749832200000, 1749834000000, 1749835800000, - 1749837600000, 1749839400000, 1749841200000, 1749843000000, - 1749844800000, 1749846600000, 1749848400000, 1749850200000, - 1749852000000, 1749853800000, 1749855600000, 1749857400000, - 1749859200000, 1749861000000, 1749862800000, 1749864600000, - 1749866400000, 1749868200000, 1749870000000, 1749871800000, - 1749873600000, 1749875400000, 1749877200000, 1749879000000, - 1749880800000, 1749882600000, 1749884400000, 1749886200000, - 1749888000000, 1749889800000, 1749891600000, 1749893400000, - 1749895200000, 1749897000000, 1749898800000, 1749900600000, - 1749902400000, 1749904200000, 1749906000000, 1749907800000, - 1749909600000, 1749911400000, 1749913200000, 1749915000000, - 1749916800000, 1749918600000, 1749920400000, 1749922200000, - 1749924000000, 1749925800000, 1749927600000, 1749929400000, - 1749931200000, 1749933000000, 1749934800000, 1749936600000, - 1749938400000, 1749940200000, 1749942000000, 1749943800000, - 1749945600000, 1749947400000, 1749949200000, 1749951000000, - 1749952800000, 1749954600000, 1749956400000, 1749958200000, - 1749960000000, 1749961800000, 1749963600000, 1749965400000, - 1749967200000, 1749969000000, 1749970800000, 1749972600000, - 1749974400000, 1749976200000, 1749978000000, 1749979800000, - 1749981600000, 1749983400000, 1749985200000, 1749987000000, - 1749988800000, 1749990600000, 1749992400000, 1749994200000, - 1749996000000, 1749997800000, 1749999600000, 1750001400000, - 1750003200000, 1750005000000, 1750006800000, 1750008600000, - 1750010400000, 1750012200000, 1750014000000, 1750015800000, - 1750017600000, 1750019400000, 1750021200000, 1750023000000, - 1750024800000, 1750026600000, 1750028400000, 1750030200000, - 1750032000000, 1750033800000, 1750035600000, 1750037400000, - 1750039200000, 1750041000000, 1750042800000, 1750044600000, - 1750046400000, 1750048200000, 1750050000000, 1750051800000, - 1750053600000, 1750055400000, 1750057200000, 1750059000000, - 1750060800000, 1750062600000, 1750064400000, 1750066200000, - 1750068000000, 1750069800000, 1750071600000, 1750073400000, - 1750075200000, 1750077000000, 1750078800000, 1750080600000, - 1750082400000, 1750084200000, 1750086000000, 1750087800000, - 1750089600000, 1750091400000, 1750093200000, 1750095000000, - 1750096800000, 1750098600000, 1750100400000, 1750102200000, - 1750104000000, - ], - [ - 11494, 11494, 11494, 11494, 11494, 11494, 11494, 11494, - 11494, 11494, 11494, 11494, 11494, 11494, 11494, 11494, - 11494, 11494, 11494, 11494, 11494, 11494, 11494, 11494, - 11494, 11494, 11494, 11494, 11494, 11494, 11494, 11494, - 11494, 11495, 11495, 11494, 11494, 11494, 11494, 11494, - 11494, 11508, 11508, 11508, 11508, 11508, 11508, 11508, - 11508, 11508, 11508, 11508, 11508, 11508, 11508, 11508, - 11508, 11508, 11508, 11508, 11508, 11508, 11508, 11508, - 11508, 11508, 11508, 11508, 11508, 11508, 11508, 11508, - 11508, 11508, 11508, 11510, 11510, 11510, 11510, 11511, - 11511, 11511, 11511, 11510, 11510, 11511, 11511, 11508, - 11508, 11510, 11510, 11509, 11509, 11509, 11509, 11512, - 11512, 11513, 11513, 11513, 11513, 11513, 11513, 11513, - 11513, 11513, 11513, 11513, 11513, 11513, 11513, 11513, - 11513, 11513, 11513, 11513, 11513, 11513, 11513, 11513, - 11513, 11513, 11513, 11513, 11513, 11513, 11513, 11514, - 11514, 11513, 11513, 11515, 11515, 11518, 11518, 11519, - 11519, 11519, 11519, 11520, 11520, 11519, 11519, 11519, - 11519, 11519, 11519, 11519, 11519, 11520, 11520, 11520, - 11520, 11520, 11520, 11520, 11520, 11521, 11521, 11521, - 11521, 11521, 11521, 11521, 11521, 11521, 11521, 11524, - 11524, 11525, 11525, 11526, 11526, 11526, 11526, 11525, - 11525, 11527, 11527, 11531, 11531, 11533, 11533, 11534, - 11534, 11531, 11531, 11532, 11532, 11532, 11532, 11532, - 11532, 11532, 11532, 11532, 11532, 11532, 11532, 11532, - 11532, 11532, 11532, 11532, 11532, 11533, 11533, 11533, - 11533, 11533, 11533, 11533, 11533, 11534, 11534, 11534, - 11534, 11535, 11535, 11536, 11536, 11536, 11536, 11536, - 11536, 11536, 11536, 11534, 11534, 11536, 11536, 11537, - 11537, 11538, 11538, 11538, 11538, 11538, 11538, 11538, - 11538, 11538, 11538, 11538, 11538, 11538, 11538, 11538, - 11538, 11538, 11538, 11538, 11538, 11538, 11538, 11538, - 11538, 11538, 11538, 11538, 11538, 11538, 11538, 11538, - 11538, 11539, 11539, 11540, 11540, 11541, 11541, 11527, - 11527, 11529, 11529, 11531, 11531, 11532, 11532, 11530, - 11530, 11529, 11529, 11529, 11529, 11529, 11529, 11529, - 11529, 11528, 11528, 11528, 11528, 11528, 11528, 11528, - 11528, 11528, 11528, 11528, 11528, 11528, 11528, 11528, - 11528, 11528, 11528, 11528, 11528, 11528, 11528, 11528, - 11528, 11528, 11528, 11528, 11528, 11528, 11528, 11528, - 11528, 11528, 11528, 11528, 11528, 11528, 11528, 11528, - 11528, 11528, 11528, 11528, 11528, 11528, 11528, 11528, - 11528, 11528, 11528, 11528, 11528, 11528, 11528, 11528, - 11528, 11528, 11528, 11528, 11528, 11528, 11528, 11528, - 11528, 11528, 11528, 11527, 11527, 11527, 11527, 11527, - 11527, 11527, 11527, 11527, 11527, 11527, 11527, 11527, - 11527, 11526, 11526, 11519, 11519, 11512, 11512, 11512, - 11512, 11512, 11512, 11512, 11512, 11512, 11512, 11512, - 11512, 11512, 11512, 11512, 11512, 11512, 11512, 11512, - 11512, 11512, 11512, 11512, 11512, 11512, 11512, 11511, - 11511, 11511, 11511, 11511, 11511, 11510, 11510, 11513, - 11513, 11513, 11513, 11513, 11513, 11514, 11514, 11514, - 11514, 11514, 11514, 11515, 11515, 11519, 11519, 11520, - 11520, 11519, 11519, 11518, 11518, 11517, 11517, 11518, - 11518, 11518, 11518, 11518, 11518, 11518, 11518, 11518, - 11518, 11519, 11519, 11519, 11519, 11519, 11519, 11519, - 11519, 11519, 11519, 11519, 11519, 11520, 11520, 11515, - 11515, 11516, 11516, 11518, 11518, 11520, 11520, 11521, - 11521, 11522, 11522, 11525, 11526, 11526, 11531, 11531, - 11531, 11531, 11531, 11531, 11531, 11531, 11532, 11532, - 11534, 11534, 11534, 11534, 11535, 11535, 11535, 11535, - 11535, 11535, 11535, 11535, 11535, 11535, 11535, 11535, - 11535, 11535, 11535, 11535, 11535, 11535, 11535, 11535, - 11536, 11536, 11535, 11535, 11536, 11536, 11538, 11538, - 11538, 11538, 11539, 11539, 11542, 11542, 11544, 11544, - 11544, 11544, 11544, 11544, 11548, 11548, 11550, 11550, - 11549, 11549, 11549, 11549, 11549, 11549, 11549, 11549, - 11550, 11550, 11550, 11550, 11550, 11550, 11550, 11550, - 11550, 11550, 11550, 11550, 11549, 11549, 11549, 11549, - 11550, 11550, 11551, 11551, 11551, 11551, 11551, 11551, - 11552, 11552, 11552, 11552, 11552, 11552, 11537, 11537, - 11537, 11537, 11536, 11536, 11537, 11537, 11537, 11537, - 11538, 11538, 11539, 11539, 11539, 11539, 11539, 11539, - 11539, 11539, 11539, 11539, 11539, 11539, 11539, 11539, - 11539, 11539, 11539, 11539, 11539, 11539, 11539, 11539, - 11538, 11538, 11537, 11537, 11537, 11537, 11537, 11536, - 11536, 11537, 11537, 11535, 11535, 11530, 11530, 11530, - 11530, 11530, 11530, 11525, 11525, 11525, 11525, 11519, - 11519, 11517, 11517, 11517, 11517, 11517, 11517, 11517, - 11517, 11517, 11517, 11517, 11517, 11517, 11517, 11517, - 11517, 11517, 11517, 11517, 11517, 11517, 11517, 11517, - 11517, 11517, 11517, 11517, 11517, 11517, 11517, 11517, - 11517, 11517, 11517, 11517, 11517, 11517, 11517, 11517, - 11517, 11517, 11517, 11517, 11517, 11517, 11517, 11517, - 11517, 11517, 11517, 11517, 11517, 11517, 11517, 11517, - 11517, 11517, 11517, 11517, 11517, 11517, 11517, 11517, - 11517, 11517, 11517, 11517, 11517, 11517, 11517, 11517, - 11517, 11517, 11517, 11517, 11517, 11517, 11517, 11517, - 11517, 11517, 11517, 11517, 11517, 11518, 11518, 11518, - 11518, 11518, 11518, 11518, 11518, 11518, 11518, 11518, - 11518, 11518, 11518, 11518, 11518, 11518, 11518, 11518, - 11518, 11518, 11518, 11518, 11518, 11518, 11518, 11518, - 11518, 11518, 11518, 11518, 11518, 11518, 11518, 11518, - 11518, 11518, 11518, 11519, 11519, 11513, 11513, 11513, - 11513, 11513, 11513, 11517, 11517, 11513, 11513, 11515, - 11515, 11514, 11514, 11519, 11519, 11520, 11520, 11520, - 11520, 11519, 11519, 11519, 11519, 11519, 11519, 11519, - 11519, 11519, 11519, 11519, 11519, 11518, 11518, 11518, - 11518, 11519, 11519, 11519, 11519, 11517, 11517, 11517, - 11518, 11520, 11519, 11519, 11521, 11522, 11522, 11522, - 11522, 11522, 11522, 11521, 11521, 11521, 11528, 11528, - 11519, 11519, 11518, 11518, 11520, 11520, 11522, 11522, - 11524, 11524, 11524, 11524, 11524, 11524, 11525, 11525, - 11525, 11525, 11525, 11525, 11525, 11525, 11525, 11525, - 11525, 11525, 11525, 11525, 11525, 11525, 11529, 11529, - 11493, 11493, 11494, 11494, 11498, 11498, 11499, 11499, - 11499, 11499, 11499, 11499, 11500, 11500, 11500, 11500, - 11502, 11502, 11504, 11504, 11507, 11507, 11517, 11517, - 11518, 11518, 11517, 11517, 11517, 11517, 11517, 11517, - 11517, 11517, 11517, 11517, 11517, 11517, 11517, 11517, - 11517, 11517, 11517, 11517, 11517, 11517, 11517, 11517, - 11520, 11520, 11520, 11520, 11521, 11521, 11521, 11521, - 11522, 11522, 11523, 11523, 11525, 11525, 11524, 11524, - 11526, 11526, 11526, 11526, 11526, 11526, 11527, 11527, - 11525, 11525, 11525, 11525, 11525, 11525, 11521, 11521, - 11521, 11521, 11521, 11521, 11521, 11521, 11521, 11521, - 11521, 11521, 11521, 11521, 11522, 11522, 11523, 11523, - 11523, 11523, 11524, 11524, 11515, 11515, 11516, 11516, - 11516, 11516, 11516, 11516, 11517, 11517, 11521, 11521, - 11427, 11427, 11427, 11427, 11428, 11428, 11428, 11428, - 11428, 11428, 11427, 11427, 11427, 11427, 11428, 11428, - 11428, 11428, 11428, 11428, 11428, 11428, 11428, 11428, - 11428, 11428, 11428, 11428, 11427, 11427, 11427, 11427, - 11427, 11427, 11427, 11427, 11427, 11427, 11427, 11427, - 11427, 11427, 11427, 11427, 11427, 11427, 11427, 11427, - 11427, 11427, 11427, 11427, 11427, 11427, 11427, 11427, - 11427, 11427, 11427, 11427, 11427, 11427, 11427, 11427, - 11427, 11427, 11427, 11427, 11427, 11427, 11427, 11427, - 11427, 11427, 11427, 11427, 11427, 11427, 11427, 11427, - 11427, 11427, 11427, 11427, 11427, 11427, 11427, 11427, - 11427, 11427, 11427, 11427, 11427, 11427, 11426, 11426, - 11426, 11426, 11425, 11425, 11425, 11425, 11425, 11425, - 11425, 11425, 11425, 11425, 11425, 11425, 11425, 11425, - 11425, 11425, 11425, 11425, 11425, 11425, 11425, 11425, - 11425, 11425, 11425, 11425, 11426, 11426, 11426, 11426, - 11427, 11427, 11429, 11429, 11430, 11431, 11431, 11432, - 11432, 11432, 11432, 11435, 11435, 11436, 11436, 11438, - 11438, 11438, 11438, 11439, 11439, 11437, 11437, 11441, - 11441, 11443, 11443, 11438, 11438, 11438, 11438, 11438, - 11438, 11439, 11439, 11439, 11439, 11439, 11439, 11439, - 11439, 11440, 11440, 11442, 11442, 11443, 11443, 11443, - 11443, 11443, 11443, 11439, 11439, 11447, 11447, 11447, - 11447, 11447, 11447, 11449, 11449, 11455, 11455, 11457, - 11458, 11459, 11458, 11458, 11459, 11459, 11459, 11459, - 11471, 11471, 11471, 11471, 11471, 11471, 11471, 11471, - 11472, 11472, 11472, 11472, 11472, 11472, 11472, 11472, - 11472, 11472, 11472, 11472, 11472, 11472, 11470, 11471, - 11472, 11472, 11472, 11473, 11473, 11461, 11463, 11464, - 11464, 11465, 11465, 11465, 11464, 11464, 11465, 11465, - 11466, 11466, 11469, 11469, 11471, 11471, 11468, 11468, - 11468, 11468, 11468, 11468, 11469, 11469, 11470, 11470, - 11470, 11470, 11470, 11470, 11470, 11470, 11470, 11470, - 11470, 11470, 11470, 11470, 11470, 11470, 11470, 11470, - 11470, 11470, 11470, 11466, 11466, 11469, 11469, 11471, - 11471, 11471, 11471, 11458, 11458, 11458, 11458, 11459, - 11459, 11463, 11463, 11464, 11464, 11464, 11464, 11464, - 11464, 11464, 11464, 11465, 11465, 11464, 11464, 11464, - 11464, 11464, 11464, 11465, 11465, 11454, 11454, 11454, - 11454, 11454, 11454, 11454, 11454, 11454, 11454, 11454, - 11454, 11456, 11456, 11458, 11458, 11460, 11460, 11460, - 11460, 11461, 11461, 11461, 11461, 11461, 11461, 11459, - 11459, 11461, 11461, 11462, 11462, 11462, 11462, 11464, - 11464, 11464, 11464, 11463, 11463, 11463, 11463, 11463, - 11463, 11462, 11462, 11462, 11462, 11462, 11462, 11461, - 11461, 11461, 11461, 11461, 11461, 11461, 11461, 11461, - 11461, 11461, 11461, 11461, 11461, 11461, 11461, 11461, - 11461, 11461, 11461, 11461, 11461, 11461, 11461, 11461, - 11461, 11461, 11461, 11462, 11462, 11463, 11463, 11463, - 11463, 11463, 11463, 11463, 11463, 11463, 11463, 11463, - 11463, 11463, 11463, 11463, 11463, 11463, 11463, 11463, - 11463, 11463, 11463, 11463, 11463, 11463, 11463, 11463, - 11463, 11463, 11463, 11463, 11463, 11463, 11463, 11463, - 11463, 11463, 11463, 11463, 11463, 11463, 11463, 11464, - 11464, 11464, 11464, 11464, 11464, 11464, 11464, 11464, - 11464, 11464, 11464, 11464, 11464, 11464, 11464, 11464, - 11464, 11464, 11464, 11464, 11464, 11464, 11464, 11465, - 11465, 11465, 11465, 11466, 11466, 11467, 11467, 11469, - 11469, 11469, 11469, 11472, 11472, 11472, 11473, 11473, - 11467, 11469, 11469, 11470, 11470, 11470, 11470, 11472, - 11472, 11477, 11477, 11477, 11477, 11475, 11475, 11475, - 11475, - ], - ], - }, - }, - { - schema: { - refId: 'A-Instant', - meta: { - type: 'timeseries-multi', - typeVersion: [0, 0], - custom: { - resultType: 'vector', - }, - executedQueryString: - 'Expr: sum(topk(1, instance_users{state="ACTIVE", plan=~"Pro|Enterprise"}) by(clientId))\nStep: 30m0s', - preferredVisualisationType: 'rawPrometheus', - }, - fields: [ - { - name: 'Time', - type: 'time', - config: {}, - }, - { - name: 'Value', - type: 'number', - config: {}, - }, - ], - }, - data: { - values: [[1750105009221], [11474]], - }, - }, -]; From a2fe19c53e38ad69a3d9476aa97f4de9a815c744 Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Thu, 19 Jun 2025 15:26:30 +0200 Subject: [PATCH 08/13] fix: remove leftover controller code --- src/lib/routes/admin-api/index.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/lib/routes/admin-api/index.ts b/src/lib/routes/admin-api/index.ts index f26d4a0771..5e966f699c 100644 --- a/src/lib/routes/admin-api/index.ts +++ b/src/lib/routes/admin-api/index.ts @@ -35,7 +35,6 @@ import { InactiveUsersController } from '../../users/inactive/inactive-users-con import { UiObservabilityController } from '../../features/ui-observability-controller/ui-observability-controller.js'; import { SearchApi } from './search/index.js'; import PersonalDashboardController from '../../features/personal-dashboard/personal-dashboard-controller.js'; -import ImpactMetricsController from '../../features/impact-metrics/impact-metrics-controller.js'; import FeatureLifecycleCountController from '../../features/feature-lifecycle/feature-lifecycle-count-controller.js'; import type { IUnleashServices } from '../../services/index.js'; import CustomMetricsController from '../../features/metrics/custom/custom-metrics-controller.js'; @@ -138,10 +137,6 @@ export class AdminApi extends Controller { '/personal-dashboard', new PersonalDashboardController(config, services).router, ); - this.app.use( - '/impact-metrics', - new ImpactMetricsController(config, services).router, - ); this.app.use( '/environments', new EnvironmentsController(config, services).router, From 83b2a998e1a4d708550d09430b6db65a89963a15 Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Thu, 19 Jun 2025 15:48:16 +0200 Subject: [PATCH 09/13] simplify time utils --- .../insights/impact-metrics/ImpactMetrics.tsx | 10 +++-- .../insights/impact-metrics/time-utils.ts | 41 ------------------- .../useImpactMetricsData.ts | 11 ++++- 3 files changed, 16 insertions(+), 46 deletions(-) diff --git a/frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx b/frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx index 776d3e37a8..9538aeeca8 100644 --- a/frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx +++ b/frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx @@ -15,7 +15,8 @@ import { useImpactMetricsMetadata } from 'hooks/api/getters/useImpactMetricsMeta import { useImpactMetricsData } from 'hooks/api/getters/useImpactMetricsData/useImpactMetricsData'; import { usePlaceholderData } from '../hooks/usePlaceholderData.js'; import { ImpactMetricsControls } from './ImpactMetricsControls.tsx'; -import { getDateRange, getDisplayFormat, getTimeUnit } from './time-utils.ts'; +import { getDisplayFormat, getTimeUnit } from './time-utils.ts'; +import { fromUnixTime } from 'date-fns'; type ImpactMetricsProps = {}; @@ -33,7 +34,7 @@ export const ImpactMetrics: FC = () => { error: metadataError, } = useImpactMetricsMetadata(); const { - data: timeSeriesData, + data: { start, end, data: timeSeriesData }, loading: dataLoading, error: dataError, } = useImpactMetricsData( @@ -86,7 +87,10 @@ export const ImpactMetrics: FC = () => { [data, isLoading], ); - const { min: minTime, max: maxTime } = getDateRange(selectedRange); + const minTime = start + ? fromUnixTime(Number.parseInt(start, 10)) + : undefined; + const maxTime = end ? fromUnixTime(Number.parseInt(end, 10)) : undefined; const placeholder = selectedSeries ? ( diff --git a/frontend/src/component/insights/impact-metrics/time-utils.ts b/frontend/src/component/insights/impact-metrics/time-utils.ts index 2f56ede49d..5d0e814427 100644 --- a/frontend/src/component/insights/impact-metrics/time-utils.ts +++ b/frontend/src/component/insights/impact-metrics/time-utils.ts @@ -14,55 +14,14 @@ export const getTimeUnit = (selectedRange: string) => { }; export const getDisplayFormat = (selectedRange: string) => { - // TODO: localized format switch (selectedRange) { case 'hour': - return 'HH:mm:ss'; case 'day': return 'HH:mm'; case 'week': - return 'MMM dd'; case 'month': return 'MMM dd'; default: return 'MMM dd HH:mm'; } }; - -export const getDateRange = ( - selectedRange: 'hour' | 'day' | 'week' | 'month', -) => { - const now = new Date(); - const endTime = now; - - switch (selectedRange) { - case 'hour': { - const startTime = new Date(now); - startTime.setMinutes(now.getMinutes() - 60, 0, 0); - return { min: startTime, max: endTime }; - } - case 'day': { - const startTime = new Date(now); - startTime.setHours(now.getHours() - 24, 0, 0, 0); - return { min: startTime, max: endTime }; - } - case 'week': { - const startTime = new Date(now); - startTime.setDate(now.getDate() - 7); - startTime.setHours(0, 0, 0, 0); - const endTimeWeek = new Date(now); - endTimeWeek.setHours(23, 59, 59, 999); - return { min: startTime, max: endTimeWeek }; - } - case 'month': { - const startTime = new Date(now); - startTime.setDate(now.getDate() - 30); - startTime.setHours(0, 0, 0, 0); - const endTimeMonth = new Date(now); - endTimeMonth.setHours(23, 59, 59, 999); - return { min: startTime, max: endTimeMonth }; - } - default: - return { min: undefined, max: undefined }; - } -}; diff --git a/frontend/src/hooks/api/getters/useImpactMetricsData/useImpactMetricsData.ts b/frontend/src/hooks/api/getters/useImpactMetricsData/useImpactMetricsData.ts index aac5a838fc..fcda63f519 100644 --- a/frontend/src/hooks/api/getters/useImpactMetricsData/useImpactMetricsData.ts +++ b/frontend/src/hooks/api/getters/useImpactMetricsData/useImpactMetricsData.ts @@ -22,7 +22,12 @@ export const useImpactMetricsData = (query?: ImpactMetricsQuery) => { const PATH = createPath(); - const { data, refetch, loading, error } = useApiGetter( + const { data, refetch, loading, error } = useApiGetter<{ + start?: string; + end?: string; + step?: string; + data: TimeSeriesData; + }>( shouldFetch ? formatApiPath(PATH) : null, shouldFetch ? () => fetcher(formatApiPath(PATH), 'Impact metrics data') @@ -34,7 +39,9 @@ export const useImpactMetricsData = (query?: ImpactMetricsQuery) => { ); return { - data: data || [], + data: data || { + data: [], + }, refetch, loading: shouldFetch ? loading : false, error, From 5352b270d6be1785275dc083af60b52dd40ff43b Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Thu, 19 Jun 2025 18:45:06 +0200 Subject: [PATCH 10/13] refactor: clean up params --- .../src/component/insights/impact-metrics/ImpactMetrics.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx b/frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx index 9538aeeca8..b766d5d9fb 100644 --- a/frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx +++ b/frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx @@ -18,9 +18,7 @@ import { ImpactMetricsControls } from './ImpactMetricsControls.tsx'; import { getDisplayFormat, getTimeUnit } from './time-utils.ts'; import { fromUnixTime } from 'date-fns'; -type ImpactMetricsProps = {}; - -export const ImpactMetrics: FC = () => { +export const ImpactMetrics: FC = () => { const theme = useTheme(); const [selectedSeries, setSelectedSeries] = useState(''); const [selectedRange, setSelectedRange] = useState< From b07d013d5036d480e8f758175044c7450182342b Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Mon, 23 Jun 2025 19:36:38 +0200 Subject: [PATCH 11/13] add search with highilght --- .../insights/impact-metrics/ImpactMetrics.tsx | 12 +++++++- .../impact-metrics/ImpactMetricsControls.tsx | 28 +++++++++++++++++-- .../useImpactMetricsMetadata.ts | 10 +++++-- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx b/frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx index b766d5d9fb..65398a22fe 100644 --- a/frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx +++ b/frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx @@ -46,6 +46,16 @@ export const ImpactMetrics: FC = () => { type: 'constant', }); + const metricSeries = useMemo(() => { + if (!metadata?.series) { + return []; + } + return Object.entries(metadata.series).map(([name, rest]) => ({ + name, + ...rest, + })); + }, [metadata]); + const data = useMemo(() => { if (!timeSeriesData.length) { return { @@ -119,7 +129,7 @@ export const ImpactMetrics: FC = () => { onRangeChange={setSelectedRange} beginAtZero={beginAtZero} onBeginAtZeroChange={setBeginAtZero} - metricSeries={metadata.series} + metricSeries={metricSeries} loading={metadataLoading} /> diff --git a/frontend/src/component/insights/impact-metrics/ImpactMetricsControls.tsx b/frontend/src/component/insights/impact-metrics/ImpactMetricsControls.tsx index 7463d8c49a..3c782ee0b8 100644 --- a/frontend/src/component/insights/impact-metrics/ImpactMetricsControls.tsx +++ b/frontend/src/component/insights/impact-metrics/ImpactMetricsControls.tsx @@ -11,6 +11,8 @@ import { TextField, Typography, } from '@mui/material'; +import type { ImpactMetricsSeries } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata'; +import { Highlighter } from 'component/common/Highlighter/Highlighter'; export interface ImpactMetricsControlsProps { selectedSeries: string; @@ -19,7 +21,7 @@ export interface ImpactMetricsControlsProps { onRangeChange: (range: 'hour' | 'day' | 'week' | 'month') => void; beginAtZero: boolean; onBeginAtZeroChange: (beginAtZero: boolean) => void; - metricSeries: string[]; + metricSeries: (ImpactMetricsSeries & { name: string })[]; loading?: boolean; } @@ -49,9 +51,29 @@ export const ImpactMetricsControls: FC = ({ onSeriesChange(newValue || '')} + getOptionLabel={(option) => option.name} + value={ + metricSeries.find((option) => option.name === selectedSeries) || + null + } + onChange={(_, newValue) => onSeriesChange(newValue?.name || '')} disabled={loading} + renderOption={(props, option, { inputValue }) => ( + + + + + {option.name} + + + + + {option.help} + + + + + )} renderInput={(params) => ( ; }; export const useImpactMetricsMetadata = () => { @@ -14,7 +18,7 @@ export const useImpactMetricsMetadata = () => { ); return { - metadata: data || { series: [], labels: [] }, + metadata: data, refetch, loading, error, From bee42187b95f94703c2ee8fc921f7676a75cf0ff Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Mon, 23 Jun 2025 22:38:42 +0200 Subject: [PATCH 12/13] account for multi-series charts --- .../insights/impact-metrics/ImpactMetrics.tsx | 124 +++++++-- .../impact-metrics/ImpactMetricsControls.tsx | 243 ++++++++++++------ .../impact-metrics/hooks/useSeriesColor.ts | 17 ++ ...{time-utils.ts => impact-metrics-utils.ts} | 23 ++ .../useImpactMetricsData.ts | 63 +++-- 5 files changed, 352 insertions(+), 118 deletions(-) create mode 100644 frontend/src/component/insights/impact-metrics/hooks/useSeriesColor.ts rename frontend/src/component/insights/impact-metrics/{time-utils.ts => impact-metrics-utils.ts} (54%) diff --git a/frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx b/frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx index 65398a22fe..035879152d 100644 --- a/frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx +++ b/frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx @@ -15,16 +15,30 @@ import { useImpactMetricsMetadata } from 'hooks/api/getters/useImpactMetricsMeta import { useImpactMetricsData } from 'hooks/api/getters/useImpactMetricsData/useImpactMetricsData'; import { usePlaceholderData } from '../hooks/usePlaceholderData.js'; import { ImpactMetricsControls } from './ImpactMetricsControls.tsx'; -import { getDisplayFormat, getTimeUnit } from './time-utils.ts'; +import { + getDisplayFormat, + getSeriesLabel, + getTimeUnit, +} from './impact-metrics-utils.ts'; import { fromUnixTime } from 'date-fns'; +import { useSeriesColor } from './hooks/useSeriesColor.ts'; export const ImpactMetrics: FC = () => { const theme = useTheme(); + const getSeriesColor = useSeriesColor(); const [selectedSeries, setSelectedSeries] = useState(''); const [selectedRange, setSelectedRange] = useState< 'hour' | 'day' | 'week' | 'month' >('day'); const [beginAtZero, setBeginAtZero] = useState(false); + const [selectedLabels, setSelectedLabels] = useState< + Record + >({}); + + const handleSeriesChange = (series: string) => { + setSelectedSeries(series); + setSelectedLabels({}); // labels are series-specific + }; const { metadata, @@ -32,12 +46,19 @@ export const ImpactMetrics: FC = () => { error: metadataError, } = useImpactMetricsMetadata(); const { - data: { start, end, data: timeSeriesData }, + data: { start, end, series: timeSeriesData, labels: availableLabels }, loading: dataLoading, error: dataError, } = useImpactMetricsData( selectedSeries - ? { series: selectedSeries, range: selectedRange } + ? { + series: selectedSeries, + range: selectedRange, + labels: + Object.keys(selectedLabels).length > 0 + ? selectedLabels + : undefined, + } : undefined, ); @@ -57,7 +78,7 @@ export const ImpactMetrics: FC = () => { }, [metadata]); const data = useMemo(() => { - if (!timeSeriesData.length) { + if (!timeSeriesData || timeSeriesData.length === 0) { return { labels: [], datasets: [ @@ -70,29 +91,75 @@ export const ImpactMetrics: FC = () => { }; } - const timestamps = timeSeriesData.map( - ([epochTimestamp]) => new Date(epochTimestamp * 1000), - ); - const values = timeSeriesData.map(([, value]) => value); + if (timeSeriesData.length === 1) { + const series = timeSeriesData[0]; + const timestamps = series.data.map( + ([epochTimestamp]) => new Date(epochTimestamp * 1000), + ); + const values = series.data.map(([, value]) => value); - return { - labels: timestamps, - datasets: [ - { - data: values, - borderColor: theme.palette.primary.main, - backgroundColor: theme.palette.primary.light, - }, - ], - }; - }, [timeSeriesData, theme]); + return { + labels: timestamps, + datasets: [ + { + data: values, + borderColor: theme.palette.primary.main, + backgroundColor: theme.palette.primary.light, + label: getSeriesLabel(series.metric), + }, + ], + }; + } else { + const allTimestamps = new Set(); + timeSeriesData.forEach((series) => { + series.data.forEach(([timestamp]) => { + allTimestamps.add(timestamp); + }); + }); + const sortedTimestamps = Array.from(allTimestamps).sort( + (a, b) => a - b, + ); + const labels = sortedTimestamps.map( + (timestamp) => new Date(timestamp * 1000), + ); + + const datasets = timeSeriesData.map((series) => { + const seriesLabel = getSeriesLabel(series.metric); + const color = getSeriesColor(seriesLabel); + + const dataMap = new Map(series.data); + + const data = sortedTimestamps.map( + (timestamp) => dataMap.get(timestamp) ?? null, + ); + + return { + label: seriesLabel, + data, + borderColor: color, + backgroundColor: color, + fill: false, + spanGaps: false, // Don't connect nulls + }; + }); + + return { + labels, + datasets, + }; + } + }, [timeSeriesData, theme, getSeriesColor]); const hasError = metadataError || dataError; const isLoading = metadataLoading || dataLoading; const shouldShowPlaceholder = !selectedSeries || isLoading || hasError; const notEnoughData = useMemo( - () => !isLoading && !data.datasets.some((d) => d.data.length > 1), - [data, isLoading], + () => + !isLoading && + (!timeSeriesData || + timeSeriesData.length === 0 || + !data.datasets.some((d) => d.data.length > 1)), + [data, isLoading, timeSeriesData], ); const minTime = start @@ -124,13 +191,16 @@ export const ImpactMetrics: FC = () => { > {!selectedSeries && !isLoading ? ( @@ -189,7 +259,15 @@ export const ImpactMetrics: FC = () => { }, plugins: { legend: { - display: false, + display: + timeSeriesData && + timeSeriesData.length > 1, + position: 'bottom' as const, + labels: { + usePointStyle: true, + boxWidth: 8, + padding: 12, + }, }, }, animations: { diff --git a/frontend/src/component/insights/impact-metrics/ImpactMetricsControls.tsx b/frontend/src/component/insights/impact-metrics/ImpactMetricsControls.tsx index 3c782ee0b8..3c9e9dc681 100644 --- a/frontend/src/component/insights/impact-metrics/ImpactMetricsControls.tsx +++ b/frontend/src/component/insights/impact-metrics/ImpactMetricsControls.tsx @@ -10,8 +10,10 @@ import { Autocomplete, TextField, Typography, + Chip, } from '@mui/material'; import type { ImpactMetricsSeries } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata'; +import type { ImpactMetricsLabels } from 'hooks/api/getters/useImpactMetricsData/useImpactMetricsData'; import { Highlighter } from 'component/common/Highlighter/Highlighter'; export interface ImpactMetricsControlsProps { @@ -23,6 +25,9 @@ export interface ImpactMetricsControlsProps { onBeginAtZeroChange: (beginAtZero: boolean) => void; metricSeries: (ImpactMetricsSeries & { name: string })[]; loading?: boolean; + selectedLabels: Record; + onLabelsChange: (labels: Record) => void; + availableLabels?: ImpactMetricsLabels; } export const ImpactMetricsControls: FC = ({ @@ -34,86 +39,166 @@ export const ImpactMetricsControls: FC = ({ onBeginAtZeroChange, metricSeries, loading = false, -}) => ( - ({ - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(3), - maxWidth: 400, - })} - > - - Select a custom metric to see its value over time. This can help you - understand the impact of your feature rollout on key outcomes, such - as system performance, usage patterns or error rates. - + selectedLabels, + onLabelsChange, + availableLabels, +}) => { + const handleLabelChange = (labelKey: string, values: string[]) => { + const newLabels = { ...selectedLabels }; + if (values.length === 0) { + delete newLabels[labelKey]; + } else { + newLabels[labelKey] = values; + } + onLabelsChange(newLabels); + }; - option.name} - value={ - metricSeries.find((option) => option.name === selectedSeries) || - null - } - onChange={(_, newValue) => onSeriesChange(newValue?.name || '')} - disabled={loading} - renderOption={(props, option, { inputValue }) => ( - - - - - {option.name} - - - - - {option.help} - - + const clearAllLabels = () => { + onLabelsChange({}); + }; + + return ( + ({ + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(3), + maxWidth: 400, + })} + > + + Select a custom metric to see its value over time. This can help + you understand the impact of your feature rollout on key + outcomes, such as system performance, usage patterns or error + rates. + + + option.name} + value={ + metricSeries.find( + (option) => option.name === selectedSeries, + ) || null + } + onChange={(_, newValue) => onSeriesChange(newValue?.name || '')} + disabled={loading} + renderOption={(props, option, { inputValue }) => ( + + + + + {option.name} + + + + + {option.help} + + + + )} + renderInput={(params) => ( + + )} + noOptionsText='No metrics available' + sx={{ minWidth: 300 }} + /> + + + Time + + + + onBeginAtZeroChange(e.target.checked)} + /> + } + label='Begin at zero' + /> + + {availableLabels && Object.keys(availableLabels).length > 0 && ( + + + + Filter by labels + + {Object.keys(selectedLabels).length > 0 && ( + + )} + + + {Object.entries(availableLabels).map( + ([labelKey, values]) => ( + + handleLabelChange(labelKey, newValues) + } + renderTags={(value, getTagProps) => + value.map((option, index) => { + const { key, ...chipProps } = + getTagProps({ index }); + return ( + + ); + }) + } + renderInput={(params) => ( + + )} + sx={{ minWidth: 300 }} + /> + ), + )} )} - renderInput={(params) => ( - - )} - noOptionsText='No metrics available' - sx={{ minWidth: 300 }} - /> - - - Time - - - - onBeginAtZeroChange(e.target.checked)} - /> - } - label='Begin at zero' - /> - -); + + ); +}; diff --git a/frontend/src/component/insights/impact-metrics/hooks/useSeriesColor.ts b/frontend/src/component/insights/impact-metrics/hooks/useSeriesColor.ts new file mode 100644 index 0000000000..a8a0f3f6e5 --- /dev/null +++ b/frontend/src/component/insights/impact-metrics/hooks/useSeriesColor.ts @@ -0,0 +1,17 @@ +import { useTheme } from '@mui/material'; + +export const useSeriesColor = () => { + const theme = useTheme(); + const colors = theme.palette.charts.series; + + return (seriesLabel: string): string => { + let hash = 0; + for (let i = 0; i < seriesLabel.length; i++) { + const char = seriesLabel.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; // Convert to 32-bit integer + } + const index = Math.abs(hash) % colors.length; + return colors[index]; + }; +}; diff --git a/frontend/src/component/insights/impact-metrics/time-utils.ts b/frontend/src/component/insights/impact-metrics/impact-metrics-utils.ts similarity index 54% rename from frontend/src/component/insights/impact-metrics/time-utils.ts rename to frontend/src/component/insights/impact-metrics/impact-metrics-utils.ts index 5d0e814427..2f62e26676 100644 --- a/frontend/src/component/insights/impact-metrics/time-utils.ts +++ b/frontend/src/component/insights/impact-metrics/impact-metrics-utils.ts @@ -25,3 +25,26 @@ export const getDisplayFormat = (selectedRange: string) => { return 'MMM dd HH:mm'; } }; + +export const getSeriesLabel = (metric: Record): string => { + const { __name__, ...labels } = metric; + + const labelParts = Object.entries(labels) + .filter(([key, value]) => key !== '__name__' && value) + .map(([key, value]) => `${key}=${value}`) + .join(', '); + + if (!__name__ && !labelParts) { + return 'Series'; + } + + if (!__name__) { + return labelParts; + } + + if (!labelParts) { + return __name__; + } + + return `${__name__} (${labelParts})`; +}; diff --git a/frontend/src/hooks/api/getters/useImpactMetricsData/useImpactMetricsData.ts b/frontend/src/hooks/api/getters/useImpactMetricsData/useImpactMetricsData.ts index fcda63f519..930611435b 100644 --- a/frontend/src/hooks/api/getters/useImpactMetricsData/useImpactMetricsData.ts +++ b/frontend/src/hooks/api/getters/useImpactMetricsData/useImpactMetricsData.ts @@ -3,9 +3,25 @@ import { formatApiPath } from 'utils/formatPath'; export type TimeSeriesData = [number, number][]; +export type ImpactMetricsLabels = Record; + +export type ImpactMetricsSeries = { + metric: Record; + data: TimeSeriesData; +}; + +export type ImpactMetricsResponse = { + start?: string; + end?: string; + step?: string; + series: ImpactMetricsSeries[]; + labels?: ImpactMetricsLabels; +}; + export type ImpactMetricsQuery = { series: string; range: 'hour' | 'day' | 'week' | 'month'; + labels?: Record; }; export const useImpactMetricsData = (query?: ImpactMetricsQuery) => { @@ -17,30 +33,45 @@ export const useImpactMetricsData = (query?: ImpactMetricsQuery) => { series: query.series, range: query.range, }); + + if (query.labels && Object.keys(query.labels).length > 0) { + // Send labels as they are - the backend will handle the formatting + const labelsParam = Object.entries(query.labels).reduce( + (acc, [key, values]) => { + if (values.length > 0) { + acc[key] = values; + } + return acc; + }, + {} as Record, + ); + + if (Object.keys(labelsParam).length > 0) { + params.append('labels', JSON.stringify(labelsParam)); + } + } + return `api/admin/impact-metrics/?${params.toString()}`; }; const PATH = createPath(); - const { data, refetch, loading, error } = useApiGetter<{ - start?: string; - end?: string; - step?: string; - data: TimeSeriesData; - }>( - shouldFetch ? formatApiPath(PATH) : null, - shouldFetch - ? () => fetcher(formatApiPath(PATH), 'Impact metrics data') - : () => Promise.resolve([]), - { - refreshInterval: 30 * 1_000, - revalidateOnFocus: true, - }, - ); + const { data, refetch, loading, error } = + useApiGetter( + shouldFetch ? formatApiPath(PATH) : null, + shouldFetch + ? () => fetcher(formatApiPath(PATH), 'Impact metrics data') + : () => Promise.resolve([]), + { + refreshInterval: 30 * 1_000, + revalidateOnFocus: true, + }, + ); return { data: data || { - data: [], + series: [], + labels: {}, }, refetch, loading: shouldFetch ? loading : false, From 663f26a12b184d6f9299eb7097f9a5d716542c6d Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Mon, 23 Jun 2025 22:43:50 +0200 Subject: [PATCH 13/13] y axis formatter --- .../insights/impact-metrics/ImpactMetrics.tsx | 11 ++++++++++- .../{impact-metrics-utils.ts => utils.ts} | 10 ++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) rename frontend/src/component/insights/impact-metrics/{impact-metrics-utils.ts => utils.ts} (81%) diff --git a/frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx b/frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx index 035879152d..86347f5241 100644 --- a/frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx +++ b/frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx @@ -19,7 +19,8 @@ import { getDisplayFormat, getSeriesLabel, getTimeUnit, -} from './impact-metrics-utils.ts'; + formatLargeNumbers, +} from './utils.ts'; import { fromUnixTime } from 'date-fns'; import { useSeriesColor } from './hooks/useSeriesColor.ts'; @@ -254,6 +255,14 @@ export const ImpactMetrics: FC = () => { }, ticks: { precision: 0, + callback: ( + value: unknown, + ): string | number => + typeof value === 'number' + ? formatLargeNumbers( + value, + ) + : (value as number), }, }, }, diff --git a/frontend/src/component/insights/impact-metrics/impact-metrics-utils.ts b/frontend/src/component/insights/impact-metrics/utils.ts similarity index 81% rename from frontend/src/component/insights/impact-metrics/impact-metrics-utils.ts rename to frontend/src/component/insights/impact-metrics/utils.ts index 2f62e26676..8c4e292d39 100644 --- a/frontend/src/component/insights/impact-metrics/impact-metrics-utils.ts +++ b/frontend/src/component/insights/impact-metrics/utils.ts @@ -48,3 +48,13 @@ export const getSeriesLabel = (metric: Record): string => { return `${__name__} (${labelParts})`; }; + +export const formatLargeNumbers = (value: number): string => { + if (value >= 1000000) { + return `${(value / 1000000).toFixed(0)}M`; + } + if (value >= 1000) { + return `${(value / 1000).toFixed(0)}k`; + } + return value.toString(); +};