diff --git a/locust/web.py b/locust/web.py index 962490ed3f..3d16911b16 100644 --- a/locust/web.py +++ b/locust/web.py @@ -578,6 +578,7 @@ def update_template_args(self): "stats_history_enabled": options and options.stats_history_enabled, "tasks": dumps({}), "extra_options": extra_options, + "run_time": options and options.run_time, "show_userclass_picker": self.userclass_picker_is_active, "available_user_classes": available_user_classes, "available_shape_classes": available_shape_classes, diff --git a/locust/webui/dist/assets/index-b0f93c80.js b/locust/webui/dist/assets/index-b0f93c80.js new file mode 100644 index 0000000000..ae3ee7c6ed --- /dev/null +++ b/locust/webui/dist/assets/index-b0f93c80.js @@ -0,0 +1,7 @@ +import{j as t,M as we,B as m,I as Y,d as ge,u as Te,r as l,a as Ce,b as w,T as u,L as p,C as v,c as $,e as ke,f as ve,g as Re,h as x,D as T,i as b,F as $e,k as De,l as Ee,A as Z,m as X,n as ee,o as te,p as Pe,q as Ae,s as Ie,P as Ne,t as Le,v as Me,w as q,x as J,y as Ue,z as Fe,E as g,G as Oe,H as Ge,J as We,K as He,N as Be,O as Ve,Q as _e,R as Ke,S as qe,U as Je,V as ze}from"./vendor-f6987cc3.js";(function(){const s=document.createElement("link").relList;if(s&&s.supports&&s.supports("modulepreload"))return;for(const a of document.querySelectorAll('link[rel="modulepreload"]'))r(a);new MutationObserver(a=>{for(const o of a)if(o.type==="childList")for(const i of o.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&r(i)}).observe(document,{childList:!0,subtree:!0});function n(a){const o={};return a.integrity&&(o.integrity=a.integrity),a.referrerPolicy&&(o.referrerPolicy=a.referrerPolicy),a.crossOrigin==="use-credentials"?o.credentials="include":a.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function r(a){if(a.ep)return;a.ep=!0;const o=n(a);fetch(a.href,o)}})();function U({open:e,onClose:s,children:n}){return t.jsx(we,{onClose:s,open:e,children:t.jsxs(m,{sx:{position:"absolute",top:"50%",left:"50%",transform:"translate(-50%, -50%)",width:"md",display:"flex",flexDirection:"column",rowGap:2,bgcolor:"background.paper",boxShadow:24,borderRadius:2,p:4},children:[t.jsx(Y,{color:"inherit",onClick:s,sx:{position:"absolute",top:1,right:1},children:t.jsx(ge,{})}),n]})})}const D=Te,Qe=Ce;function C(e){const s=Qe();return l.useCallback(n=>{s(e(n))},[e,s])}const Ye=[{name:"Carl Byström",website:"http://cgbystrom.com/",social:{handle:"@cgbystrom",link:"https://twitter.com/cgbystrom/"}},{name:"Jonatan Heyman",website:"http://heyman.info/",social:{handle:"@jonatanheyman",link:"https://twitter.com/jonatanheyman/"}},{name:"Joakim Hamrén",social:{handle:"@jahaaja",link:"https://twitter.com/Jahaaja/"}},{name:"ESN Social Software",website:"http://esn.me/",social:{handle:"@uprise_ea",link:"https://twitter.com/uprise_ea"}},{name:"Hugo Heyman",social:{handle:"@hugoheyman",link:"https://twitter.com/hugoheyman/"}}];function Ze(){const[e,s]=l.useState(!1),n=D(({swarm:r})=>r.version);return t.jsxs(t.Fragment,{children:[t.jsx(m,{sx:{display:"flex",justifyContent:"flex-end"},children:t.jsx(w,{color:"inherit",onClick:()=>s(!0),variant:"text",children:"About"})}),t.jsxs(U,{onClose:()=>s(!1),open:e,children:[t.jsxs("div",{children:[t.jsx(u,{component:"h2",variant:"h4",children:"About"}),t.jsx(u,{component:"p",variant:"subtitle1",children:"The original idea for Locust was Carl Byström's who made a first proof of concept in June 2010. Jonatan Heyman picked up Locust in January 2011, implemented the current concept of Locust classes and made it work distributed across multiple machines."}),t.jsx(u,{component:"p",sx:{mt:2},variant:"subtitle1",children:"Jonatan, Carl and Joakim Hamrén has continued the development of Locust at their job, ESN Social Software, who have adopted Locust as an inhouse Open Source project."})]}),t.jsxs("div",{children:[t.jsx(u,{component:"h2",variant:"h4",children:"Authors and Copyright"}),t.jsx(m,{sx:{display:"flex",flexDirection:"column",rowGap:.5},children:Ye.map(({name:r,website:a,social:{handle:o,link:i}},c)=>t.jsxs("div",{children:[a?t.jsx(p,{href:a,children:r}):r,t.jsxs(m,{sx:{display:"inline",ml:.5},children:["(",t.jsx(p,{href:i,children:o}),")"]})]},`author-${c}`))})]}),t.jsxs("div",{children:[t.jsx(u,{component:"h2",variant:"h4",children:"License"}),t.jsx(u,{component:"p",variant:"subtitle1",children:"Open source licensed under the MIT license."})]}),t.jsxs("div",{children:[t.jsx(u,{component:"h2",variant:"h4",children:"Version"}),t.jsx(p,{href:`https://github.com/locustio/locust/releases/tag/${n}`,children:n})]}),t.jsxs("div",{children:[t.jsx(u,{component:"h2",variant:"h4",children:"Website"}),t.jsx(p,{href:"https://locust.io/",children:"https://locust.io"})]})]})]})}function Xe(){return t.jsx(m,{component:"nav",sx:{position:"fixed",bottom:0,width:"100%"},children:t.jsx(v,{maxWidth:"xl",sx:{display:"flex",justifyContent:"flex-end"},children:t.jsx(Ze,{})})})}const et={isDarkMode:!1},se=$({name:"theme",initialState:et,reducers:{setIsDarkMode:(e,{payload:s})=>({...e,isDarkMode:s})}}),tt=se.actions,st=se.reducer,k={DARK:"dark",LIGHT:"light"},nt=e=>ke({palette:{mode:e,primary:{main:"#15803d"},success:{main:"#00C853"}}});function rt(){const e=D(({theme:{isDarkMode:r}})=>r),s=C(tt.setIsDarkMode);l.useEffect(()=>{s(localStorage.theme===k.DARK||!("theme"in localStorage)&&window.matchMedia("(prefers-color-scheme: dark)").matches)},[]);const n=()=>{localStorage.theme=e?k.LIGHT:k.DARK,s(!e)};return t.jsx(Y,{color:"inherit",onClick:n,children:e?t.jsx(ve,{}):t.jsx(Re,{})})}const h={READY:"ready",RUNNING:"running",STOPPED:"stopped",SPAWNING:"spawning",CLEANUP:"cleanup",STOPPING:"stopping",MISSING:"missing"};function at({isDistributed:e,state:s,host:n,totalRps:r,failRatio:a,userCount:o,workerCount:i}){return t.jsxs(m,{sx:{display:"flex",columnGap:2},children:[t.jsxs(m,{sx:{display:"flex",flexDirection:"column"},children:[t.jsx(u,{variant:"button",children:"Host"}),t.jsx(u,{children:n})]}),t.jsx(T,{flexItem:!0,orientation:"vertical"}),t.jsxs(m,{sx:{display:"flex",flexDirection:"column"},children:[t.jsx(u,{variant:"button",children:"Status"}),t.jsx(u,{variant:"button",children:s})]}),s===h.RUNNING&&t.jsxs(t.Fragment,{children:[t.jsx(T,{flexItem:!0,orientation:"vertical"}),t.jsxs(m,{sx:{display:"flex",flexDirection:"column",alignItems:"center"},children:[t.jsx(u,{variant:"button",children:"Users"}),t.jsx(u,{variant:"button",children:o})]})]}),e&&t.jsxs(t.Fragment,{children:[t.jsx(T,{flexItem:!0,orientation:"vertical"}),t.jsxs(m,{sx:{display:"flex",flexDirection:"column",alignItems:"center"},children:[t.jsx(u,{variant:"button",children:"Workers"}),t.jsx(u,{variant:"button",children:i})]})]}),t.jsx(T,{flexItem:!0,orientation:"vertical"}),t.jsxs(m,{sx:{display:"flex",flexDirection:"column",alignItems:"center"},children:[t.jsx(u,{variant:"button",children:"RPS"}),t.jsx(u,{variant:"button",children:r})]}),t.jsx(T,{flexItem:!0,orientation:"vertical"}),t.jsxs(m,{sx:{display:"flex",flexDirection:"column",alignItems:"center"},children:[t.jsx(u,{variant:"button",children:"Failures"}),t.jsx(u,{variant:"button",children:`${a}%`})]})]})}const ot=({swarm:{isDistributed:e,state:s,host:n,workerCount:r},ui:{totalRps:a,failRatio:o,userCount:i}})=>({isDistributed:e,state:s,host:n,totalRps:a,failRatio:o,userCount:i,workerCount:r}),it=x(ot)(at),ct=e=>!Object.keys(e).length;function lt(e,s){return{...e,...s}}const ut=e=>{const s=new URLSearchParams;for(const[n,r]of Object.entries(e))if(Array.isArray(r))for(const a of r)s.append(n,a);else s.append(n,r);return s},F=(e,s)=>Object.entries(s).reduce((n,[r,a])=>({...n,[r]:[...n[r]||[],a]}),e);function ne(e,s,n){return s&&(Array.isArray(s)?s.map(r=>ne(e,r,n)):typeof s=="object"?O(s,n):s)}const O=(e,s)=>Object.entries(e).reduce((n,[r,a])=>({...n,[s(r)]:ne(e,a,s)}),{}),re=e=>e.replace(/_([a-z0-9])/g,(s,n)=>n.toUpperCase()),ae=e=>e[0]===e[0].toUpperCase()?e:e.replace(/([a-z0-9])([A-Z0-9])/g,"$1_$2").toLowerCase(),G=e=>O(e,re),z=e=>O(e,ae),dt=e=>e.replace(/([a-z0-9])([A-Z0-9])/g,"$1 $2").replace(/^./,s=>s.toUpperCase()),oe=(e,{shouldTransformKeys:s=!0}={})=>e?Object.entries(e).reduce((n,[r,a])=>{if(!a)return n;const o=s?ae(r):r,i=encodeURI(String(a).replace("#",""));return n?`${n}&${o}=${i}`:`?${o}=${i}`},""):"",mt=e=>e.substring(1).split("&").reduce((s,n)=>{const[r,a]=n.split("=");return{...s,[r]:a}},{}),W={GET:"GET",POST:"POST",PUT:"PUT",DELETE:"DELETE"},ht=({body:e,form:s})=>e?s?ut(z(e)):JSON.stringify(z(e)):null,xt=({method:e,body:s,form:n})=>({headers:{"Content-Type":n?"application/x-www-form-urlencoded":"application/json"},method:e,body:ht({body:s,form:n})});async function S(e,{method:s=W.GET,body:n=null,query:r=null,form:a=!1}={}){try{const o=oe(r),i=xt({method:s,body:n,form:a}),c=await fetch(`${e}${o}`,i),d=G(await c.json());if(d.statusCode>=400)throw new Error(`Network Error: Status ${d.statusCode} ${d.message}`);return d}catch(o){return console.error("Network Error:",o),o}}function ie({children:e,onSubmit:s}){const n=l.useCallback(async r=>{r.preventDefault();const a=new FormData(r.target),o={};for(const[i,c]of a.entries())o.hasOwnProperty(i)?(Array.isArray(o[i])||(o[i]=[o[i]]),o[i].push(c)):o[i]=c;s(o)},[s]);return t.jsx("form",{onSubmit:n,children:e})}function pt({onSubmit:e,spawnRate:s,userCount:n}){const r=a=>{e(),S("swarm",{method:W.POST,body:a,form:!0})};return t.jsxs(v,{maxWidth:"md",sx:{my:2},children:[t.jsx(u,{component:"h2",noWrap:!0,variant:"h6",children:"Edit running load test"}),t.jsx(ie,{onSubmit:r,children:t.jsxs(m,{sx:{my:2,display:"flex",flexDirection:"column",rowGap:4},children:[t.jsx(b,{defaultValue:n||1,label:"Number of users (peak concurrency)",name:"userCount"}),t.jsx(b,{defaultValue:s||1,label:"Ramp Up (users started/second)",name:"spawnRate"}),t.jsx(w,{size:"large",type:"submit",variant:"contained",children:"Update Swarm"})]})})]})}const ft=({swarm:{spawnRate:e,userCount:s}})=>({spawnRate:e,userCount:s}),jt=x(ft)(pt);function yt(){const[e,s]=l.useState(!1);return t.jsxs(t.Fragment,{children:[t.jsx(w,{color:"secondary",onClick:()=>s(!0),type:"button",variant:"contained",children:"Edit"}),t.jsx(U,{onClose:()=>s(!1),open:e,children:t.jsx(jt,{onSubmit:()=>s(!1)})})]})}function N({label:e,name:s,options:n,multiple:r=!1,defaultValue:a,sx:o}){return t.jsxs($e,{sx:o,children:[t.jsx(De,{htmlFor:s,shrink:!0,children:e}),t.jsx(Ee,{defaultValue:a||r&&n||n[0],label:e,multiple:r,name:s,native:!0,children:n.map((i,c)=>t.jsx("option",{value:i,children:i},`option-${i}-${c}`))})]})}const Q=e=>e.defaultValue!==null&&typeof e.defaultValue!="boolean";function bt({label:e,defaultValue:s,choices:n,helpText:r,isSecret:a}){const o=dt(e),i=r?`${o} (${r})`:o;return n?t.jsx(N,{defaultValue:s,label:i,name:e,options:n,sx:{width:"100%"}}):t.jsx(b,{label:i,name:e,sx:{width:"100%"},type:a?"password":"text",value:s})}function St({extraOptions:e}){const s=l.useMemo(()=>Object.entries(e).reduce((r,[a,o])=>Q(o)?[...r,{label:a,...o}]:r,[]),[e]),n=l.useMemo(()=>Object.keys(e).reduce((r,a)=>Q(e[a])?r:[...r,a],[]),[e]);return t.jsxs(Z,{children:[t.jsx(X,{expandIcon:t.jsx(ee,{}),children:t.jsx(u,{children:"Custom parameters"})}),t.jsx(te,{children:t.jsxs(m,{sx:{display:"flex",flexDirection:"column",rowGap:4},children:[s.map((r,a)=>t.jsx(bt,{...r},`valid-parameter-${a}`)),t.jsx(m,{children:n&&t.jsxs(t.Fragment,{children:[t.jsx(u,{children:"The following custom parameters can't be set in the Web UI, because it is a boolean or None type:"}),t.jsx("ul",{children:n.map((r,a)=>t.jsx("li",{children:t.jsx(u,{children:r})},`invalid-parameter-${a}`))})]})})]})})]})}function H(e,{payload:s}){return lt(e,s)}const wt=G(window.templateArgs),ce=$({name:"swarm",initialState:wt,reducers:{setSwarm:H}}),le=ce.actions,gt=ce.reducer;function Tt({availableShapeClasses:e,availableUserClasses:s,host:n,extraOptions:r,isShape:a,overrideHostWarning:o,runTime:i,setSwarm:c,showUserclassPicker:d,spawnRate:y,userCount:f}){const j=E=>{c({state:h.RUNNING}),S("swarm",{method:W.POST,body:E,form:!0})};return t.jsxs(v,{maxWidth:"md",sx:{my:2},children:[t.jsx(u,{component:"h2",noWrap:!0,variant:"h6",children:"Start new load test"}),t.jsx(ie,{onSubmit:j,children:t.jsxs(m,{sx:{my:2,display:"flex",flexDirection:"column",rowGap:4},children:[d&&t.jsxs(t.Fragment,{children:[t.jsx(N,{label:"User Classes",multiple:!0,name:"userClasses",options:s}),t.jsx(N,{label:"Shape Class",name:"shapeClass",options:e})]}),t.jsx(b,{defaultValue:a&&"-"||f||1,disabled:!!a,label:"Number of users (peak concurrency)",name:"userCount"}),t.jsx(b,{defaultValue:a&&"-"||y||1,disabled:!!a,label:"Ramp Up (users started/second)",name:"spawnRate",title:"Disabled for tests using LoadTestShape class"}),t.jsx(b,{defaultValue:n,label:`Host ${o?"(setting this will override the host for the User classes)":""}`,name:"host",title:"Disabled for tests using LoadTestShape class"}),t.jsxs(Z,{children:[t.jsx(X,{expandIcon:t.jsx(ee,{}),children:t.jsx(u,{children:"Advanced options"})}),t.jsx(te,{children:t.jsx(b,{defaultValue:i,label:"Run time (e.g. 20, 20s, 3m, 2h, 1h20m, 3h30m10s, etc.)",name:"runTime",sx:{width:"100%"}})})]}),!ct(r)&&t.jsx(St,{extraOptions:r}),t.jsx(w,{size:"large",type:"submit",variant:"contained",children:"Start Swarm"})]})})]})}const Ct=({swarm:{availableShapeClasses:e,availableUserClasses:s,extraOptions:n,isShape:r,host:a,numUsers:o,overrideHostWarning:i,runTime:c,spawnRate:d,showUserclassPicker:y,userCount:f}})=>({availableShapeClasses:e,availableUserClasses:s,extraOptions:n,isShape:r,host:a,overrideHostWarning:i,showUserclassPicker:y,numUsers:o,runTime:c,spawnRate:d,userCount:f}),kt={setSwarm:le.setSwarm},ue=x(Ct,kt)(Tt);function vt(){const[e,s]=l.useState(!1);return t.jsxs(t.Fragment,{children:[t.jsx(w,{color:"success",onClick:()=>s(!0),type:"button",variant:"contained",children:"New"}),t.jsx(U,{onClose:()=>s(!1),open:e,children:t.jsx(ue,{})})]})}function Rt(){const e=()=>{S("stats/reset")};return t.jsx(w,{color:"warning",onClick:e,type:"button",variant:"contained",children:"Reset"})}function $t(){const[e,s]=l.useState(!1);l.useEffect(()=>{s(!1)},[]);const n=()=>{S("stop"),s(!0)};return t.jsx(w,{color:"error",disabled:e,onClick:n,type:"button",variant:"contained",children:e?"Loading":"Stop"})}function Dt(){const e=D(({swarm:s})=>s.state);return e===h.READY?null:e===h.RUNNING?t.jsxs(m,{sx:{display:"flex",columnGap:2},children:[t.jsx(yt,{}),t.jsx($t,{}),t.jsx(Rt,{})]}):t.jsx(vt,{})}function Et(){return t.jsx(Pe,{position:"static",children:t.jsx(v,{maxWidth:"xl",children:t.jsxs(Ae,{sx:{display:"flex",justifyContent:"space-between"},children:[t.jsxs(p,{color:"inherit",href:"#",sx:{display:"flex",alignItems:"center",columnGap:2},underline:"none",children:[t.jsx("img",{height:"52",src:"/assets/logo.png",width:"52"}),t.jsx(u,{component:"h1",noWrap:!0,sx:{fontWeight:700,display:"flex",alignItems:"center"},variant:"h3",children:"Locust"})]}),t.jsxs(m,{sx:{display:"flex",columnGap:6},children:[t.jsx(it,{}),t.jsx(Dt,{}),t.jsx(rt,{})]})]})})})}function Pt({children:e}){return t.jsxs(t.Fragment,{children:[t.jsx(Et,{}),t.jsx("main",{children:e}),t.jsx(Xe,{})]})}const L=(e,s=0)=>{const n=Math.pow(10,s);return Math.round(e*n)/n};function R({rows:e,structure:s}){return t.jsx(Ie,{component:Ne,children:t.jsxs(Le,{children:[t.jsx(Me,{children:t.jsx(q,{children:s.map(({title:n,key:r})=>t.jsx(J,{children:n},`table-head-${r}`))})}),t.jsx(Ue,{children:e.map((n,r)=>t.jsx(q,{children:s.map(({key:a,round:o},i)=>t.jsx(J,{children:o?L(n[a],o):n[a]},`table-row=${i}`))},`${n.name}-${r}`))})]})})}function At({rows:e,tableStructure:s}){return s?t.jsx(R,{rows:e,structure:s}):null}const It=({swarm:{extendedTables:e},ui:{extendedStats:s},url:{query:n}})=>{const r=n&&n.tab&&e&&e.find(({key:o})=>o===n.tab),a=n&&n.tab&&s&&s.find(({key:o})=>o===n.tab);return{tableStructure:r?r.structure.map(({key:o,...i})=>({key:re(o),...i})):null,rows:a?a.data:[]}},Nt=x(It)(At),Lt=[{key:"count",title:"# occurrences"},{key:"traceback",title:"Traceback"}];function Mt({exceptions:e}){return t.jsx(R,{rows:e,structure:Lt})}const Ut=({ui:{exceptions:e}})=>({exceptions:e}),Ft=x(Ut)(Mt),Ot=[{key:"occurrences",title:"# Failures"},{key:"method",title:"Method"},{key:"name",title:"Name"},{key:"error",title:"Message"}];function Gt({errors:e}){return t.jsx(R,{rows:e,structure:Ot})}const Wt=({ui:{errors:e}})=>({errors:e}),Ht=x(Wt)(Gt);function Bt({extendedCsvFiles:e,statsHistoryEnabled:s}){return t.jsxs(Fe,{sx:{display:"flex",flexDirection:"column"},children:[t.jsx(g,{children:t.jsx(p,{href:"/stats/requests/csv",children:"Download requests CSV"})}),s&&t.jsx(g,{children:t.jsx(p,{href:"/stats/requests_full_history/csv",children:"Download full request statistics history CSV"})}),t.jsx(g,{children:t.jsx(p,{href:"/stats/failures/csv",children:"Download failures CSV"})}),t.jsx(g,{children:t.jsx(p,{href:"/exceptions/csv",children:"Download exceptions CSV"})}),t.jsx(g,{children:t.jsx(p,{href:"/stats/report",target:"_blank",children:"Download Report"})}),e&&e.map(({href:n,title:r})=>t.jsx(g,{children:t.jsx(p,{href:n,children:r})}))]})}const Vt=({swarm:{extendedCsvFiles:e,statsHistoryEnabled:s}})=>({extendedCsvFiles:e,statsHistoryEnabled:s}),_t=x(Vt)(Bt),Kt=[{key:"method",title:"Type"},{key:"name",title:"Name"},{key:"numRequests",title:"# Requests"},{key:"numFailures",title:"# Fails"},{key:"medianResponseTime",title:"Median (ms)",round:2},{key:"ninetiethResponseTime",title:"90%ile (ms)"},{key:"ninetyNinthResponseTime",title:"99%ile (ms)"},{key:"avgResponseTime",title:"Average (ms)",round:2},{key:"minResponseTime",title:"Min (ms)"},{key:"maxResponseTime",title:"Max (ms)"},{key:"avgContentLength",title:"Average size (bytes)",round:2},{key:"currentRps",title:"Current RPS",round:2},{key:"currentFailPerSec",title:"Current Failures/s"}];function qt({stats:e}){return t.jsx(R,{rows:e,structure:Kt})}const Jt=({ui:{stats:e}})=>({stats:e}),zt=x(Jt)(qt),Qt=({charts:e,title:s,seriesData:n})=>({legend:{icon:"circle",inactiveColor:"#b3c3bc",textStyle:{color:"#b3c3bc"}},title:{text:s,x:10,y:10},tooltip:{trigger:"axis",formatter:r=>r&&Array.isArray(r)&&r.length>0&&r.some(a=>!!a.value)?r.reduce((a,{color:o,seriesName:i,value:c})=>` + ${a} +
+ + ${i}: ${c} + + `,""):"No data",axisPointer:{animation:!0},textStyle:{color:"#b3c3bc",fontSize:13},backgroundColor:"rgba(21,35,28, 0.93)",borderWidth:0,extraCssText:"z-index:1;"},xAxis:{type:"category",splitLine:{show:!1},axisLine:{lineStyle:{color:"#5b6f66"}},data:e.time},yAxis:{type:"value",boundaryGap:[0,"5%"],splitLine:{show:!1},axisLine:{lineStyle:{color:"#5b6f66"}}},series:n,grid:{x:60,y:70,x2:40,y2:40},color:["#00ca5a","#ff6d6d"],toolbox:{feature:{saveAsImage:{name:s.replace(/\s+/g,"_").toLowerCase()+"_"+new Date().getTime()/1e3,title:"Download as PNG",emphasis:{iconStyle:{textPosition:"left"}}}}}}),Yt=({charts:e,lines:s})=>s.map(({key:n,name:r})=>({name:r,type:"line",showSymbol:!0,data:e[n]})),Zt=e=>({symbol:"none",label:{formatter:s=>`Run #${s.dataIndex+1}`},lineStyle:{color:"#5b6f66"},data:(e.markers||[]).map(s=>({xAxis:s}))});Oe("locust",{backgroundColor:"#27272a",xAxis:{lineColor:"#f00"},textStyle:{color:"#b3c3bc"},title:{textStyle:{color:"#b3c3bc"}}});function Xt({charts:e,title:s,lines:n}){const[r,a]=l.useState(null),o=l.useRef(null);return l.useEffect(()=>{if(!o.current)return;const i=Ge(o.current,"locust");return i.setOption(Qt({charts:e,title:s,seriesData:Yt({charts:e,lines:n})})),a(i),()=>{We(i)}},[o]),l.useEffect(()=>{const i=n.every(({key:c})=>!!e[c]);r&&i&&r.setOption({xAxis:{data:e.time},series:n.map(({key:c},d)=>({data:e[c],...d===0?{markLine:Zt(e)}:{}}))})},[e,r,n]),t.jsx("div",{ref:o,style:{width:"100%",height:"300px"}})}const es=({ui:{charts:e}})=>({charts:e}),ts=x(es)(Xt),ss=[{title:"Total Requests per Second",lines:[{name:"RPS",key:"currentRps"},{name:"Failures/s",key:"currentFailPerSec"}]},{title:"Response Times (ms)",lines:[{name:"Median Response Time",key:"responseTimePercentile1"},{name:"95% percentile",key:"responseTimePercentile2"}]},{title:"Number of Users",lines:[{name:'"Number of Users"',key:"userCount"}]}];function ns(){return t.jsx("div",{children:ss.map((e,s)=>t.jsx(ts,{...e},`swarm-chart-${s}`))})}function rs(e){return(e*100).toFixed(1)+"%"}function M({classRatio:e}){return t.jsx("ul",{children:Object.entries(e).map(([s,{ratio:n,tasks:r}])=>t.jsxs("li",{children:[`${rs(n)} ${s}`,r&&t.jsx(M,{classRatio:r})]},`nested-ratio-${s}`))})}function as({ratios:{perClass:e,total:s}}){return!e&&!s?null:t.jsxs("div",{children:[e&&t.jsxs(t.Fragment,{children:[t.jsx("h3",{children:"Ratio Per Class"}),t.jsx(M,{classRatio:e})]}),s&&t.jsxs(t.Fragment,{children:[t.jsx("h3",{children:"Total Ratio"}),t.jsx(M,{classRatio:s})]})]})}const os=({ui:{ratios:e}})=>({ratios:e}),is=x(os)(as),cs=[{key:"id",title:"Worker"},{key:"state",title:"State"},{key:"userCount",title:"# users"},{key:"cpuUsage",title:"CPU usage"},{key:"memoryUsage",title:"Memory usage"}];function ls({workers:e=[]}){return t.jsx(R,{rows:e,structure:cs})}const us=({ui:{workers:e}})=>({workers:e}),ds=x(us)(ls),ms=[{component:zt,key:"stats",title:"Statistics"},{component:ns,key:"charts",title:"Charts"},{component:Ht,key:"failures",title:"Failures"},{component:Ft,key:"exceptions",title:"Exceptions"},{component:is,key:"ratios",title:"Current Ratio"},{component:_t,key:"reports",title:"Download Data"}],hs=[{component:ds,key:"workers",title:"Workers",shouldDisplayTab:e=>e.swarm.isDistributed}],xs=e=>{const s=new URL(window.location.href),n=`${s.origin}${s.pathname}${oe(e,{shouldTransformKeys:!1})}`;window.history.pushState({path:n},"",n)},ps=()=>window.location.search?mt(window.location.search):null,fs={query:ps()},de=$({name:"url",initialState:fs,reducers:{setUrl:H}}),js=de.actions,ys=de.reducer;function bs({currentTabIndexFromQuery:e,setUrl:s,tabs:n}){const[r,a]=l.useState(e),o=(i,c)=>{const d=n[c].key;xs({tab:d}),s({query:{tab:d}}),a(c)};return t.jsxs(v,{maxWidth:"xl",children:[t.jsx(m,{sx:{mb:2},children:t.jsx(He,{onChange:o,value:r,children:n.map(({title:i},c)=>t.jsx(Be,{label:i},`tab-${c}`))})}),n.map(({component:i=Nt},c)=>r===c&&t.jsx(i,{},`tabpabel-${c}`))]})}const Ss=e=>{const{swarm:{extendedTabs:s=[]},url:{query:n}}=e,r=hs.filter(({shouldDisplayTab:o})=>o(e)),a=[...ms,...r,...s];return{tabs:a,currentTabIndexFromQuery:n&&n.tab?a.findIndex(({key:o})=>o===n.tab):0}},ws={setUrl:js.setUrl},gs=x(Ss,ws)(bs);function P(e,{execute:s=!1}={}){const[n,r]=l.useState(!0),[a,o]=l.useState(null),[i,c]=l.useState(null),d=l.useCallback((...y)=>(o(null),c(null),e(...y).then(f=>{o(f),r(!1)}).catch(f=>{c(f),r(!1)})),[e]);return l.useEffect(()=>{s&&d()},[d,s]),{execute:d,isLoading:n,value:a,error:i}}function A(e,s,{shouldRunInterval:n}={shouldRunInterval:!0}){const r=l.useRef(e);l.useEffect(()=>{r.current=e},[e]),l.useEffect(()=>{if(!n)return;const a=setInterval(()=>r.current(),s);return()=>{clearInterval(a)}},[s,n])}const Ts=e=>e[e.length-1],Cs={totalRps:0,failRatio:0,stats:[],errors:[],exceptions:[],charts:G(window.templateArgs).history.reduce(F,{}),ratios:{},userCount:0},ks=(e,s)=>F(e,{currentRps:{value:null},currentFailPerSec:{value:null},responseTimePercentile1:{value:null},responseTimePercentile2:{value:null},userCount:{value:null},time:s}),me=$({name:"ui",initialState:Cs,reducers:{setUi:H,updateCharts:(e,{payload:s})=>({...e,charts:F(e.charts,s)}),updateChartMarkers:(e,{payload:s})=>({...e,charts:{...ks(e.charts,s.length?Ts(s):e.charts.time[0]),markers:e.charts.markers?[...e.charts.markers,s]:[e.charts.time[0],s]}})}}),I=me.actions,vs=me.reducer;function Rs(){const e=C(le.setSwarm),s=C(I.setUi),n=C(I.updateCharts),r=C(I.updateChartMarkers),a=D(({swarm:j})=>j),o=l.useRef(a.state),[i,c]=l.useState(!1),{execute:d}=P(async()=>{const{extendedStats:j,state:E,stats:he,errors:xe,totalRps:pe,failRatio:B,workers:fe,currentResponseTimePercentile1:je,currentResponseTimePercentile2:ye,userCount:V}=await S("stats/requests");E===h.STOPPED&&e({state:h.STOPPED});const _=new Date().toLocaleTimeString();i&&(c(!1),r(_));const K=L(pe,2),be=L(B*100),Se={currentRps:K,currentFailPerSec:B,responseTimePercentile1:je,responseTimePercentile2:ye,userCount:V,time:_};s({extendedStats:j,stats:he,errors:xe,totalRps:K,failRatio:be,workers:fe,userCount:V}),n(Se)}),{execute:y}=P(async()=>{const j=await S("tasks");s({ratios:j})}),{execute:f}=P(async()=>{const{exceptions:j}=await S("exceptions");s({exceptions:j})});A(d,2e3,{shouldRunInterval:a.state===h.RUNNING}),A(y,5e3,{shouldRunInterval:a.state===h.RUNNING}),A(f,5e3,{shouldRunInterval:a.state===h.RUNNING}),l.useEffect(()=>{a.state===h.RUNNING&&o.current===h.STOPPED&&c(!0),o.current=a.state},[a.state,o]),l.useEffect(()=>{a.state===h.STOPPED&&(d(),y())},[])}function $s({isDarkMode:e,swarmState:s}){Rs();const n=l.useMemo(()=>nt(e?k.DARK:k.LIGHT),[e]);return t.jsxs(Ve,{theme:n,children:[t.jsx(_e,{}),t.jsx(Pt,{children:s===h.READY?t.jsx(ue,{}):t.jsx(gs,{})})]})}const Ds=({swarm:{state:e},theme:{isDarkMode:s}})=>({isDarkMode:s,swarmState:e}),Es=x(Ds)($s),Ps=Ke({swarm:gt,theme:st,ui:vs,url:ys}),As=qe({reducer:Ps}),Is=Je.createRoot(document.getElementById("root"));Is.render(t.jsx(ze,{store:As,children:t.jsx(Es,{})})); diff --git a/locust/webui/dist/assets/index-c3bc88fd.js b/locust/webui/dist/assets/index-c3bc88fd.js deleted file mode 100644 index 82720b9a9b..0000000000 --- a/locust/webui/dist/assets/index-c3bc88fd.js +++ /dev/null @@ -1,7 +0,0 @@ -import{j as t,M as we,B as m,I as Q,d as ge,u as Te,r as l,a as Ce,b as w,T as u,L as p,C as v,c as $,e as ke,f as ve,g as Re,h as x,D as T,i as b,F as $e,k as De,l as Ee,A as Y,m as Z,n as X,o as ee,p as Pe,q as Ae,s as Ie,P as Ne,t as Le,v as Me,w as K,x as q,y as Ue,z as Fe,E as g,G as Oe,H as Ge,J as We,K as He,N as Be,O as _e,Q as Ve,R as Ke,S as qe,U as Je,V as ze}from"./vendor-f6987cc3.js";(function(){const s=document.createElement("link").relList;if(s&&s.supports&&s.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))r(o);new MutationObserver(o=>{for(const a of o)if(a.type==="childList")for(const i of a.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&r(i)}).observe(document,{childList:!0,subtree:!0});function n(o){const a={};return o.integrity&&(a.integrity=o.integrity),o.referrerPolicy&&(a.referrerPolicy=o.referrerPolicy),o.crossOrigin==="use-credentials"?a.credentials="include":o.crossOrigin==="anonymous"?a.credentials="omit":a.credentials="same-origin",a}function r(o){if(o.ep)return;o.ep=!0;const a=n(o);fetch(o.href,a)}})();function M({open:e,onClose:s,children:n}){return t.jsx(we,{onClose:s,open:e,children:t.jsxs(m,{sx:{position:"absolute",top:"50%",left:"50%",transform:"translate(-50%, -50%)",width:"md",display:"flex",flexDirection:"column",rowGap:2,bgcolor:"background.paper",boxShadow:24,borderRadius:2,p:4},children:[t.jsx(Q,{color:"inherit",onClick:s,sx:{position:"absolute",top:1,right:1},children:t.jsx(ge,{})}),n]})})}const D=Te,Qe=Ce;function C(e){const s=Qe();return l.useCallback(n=>{s(e(n))},[e,s])}const Ye=[{name:"Carl Byström",website:"http://cgbystrom.com/",social:{handle:"@cgbystrom",link:"https://twitter.com/cgbystrom/"}},{name:"Jonatan Heyman",website:"http://heyman.info/",social:{handle:"@jonatanheyman",link:"https://twitter.com/jonatanheyman/"}},{name:"Joakim Hamrén",social:{handle:"@jahaaja",link:"https://twitter.com/Jahaaja/"}},{name:"ESN Social Software",website:"http://esn.me/",social:{handle:"@uprise_ea",link:"https://twitter.com/uprise_ea"}},{name:"Hugo Heyman",social:{handle:"@hugoheyman",link:"https://twitter.com/hugoheyman/"}}];function Ze(){const[e,s]=l.useState(!1),n=D(({swarm:r})=>r.version);return t.jsxs(t.Fragment,{children:[t.jsx(m,{sx:{display:"flex",justifyContent:"flex-end"},children:t.jsx(w,{color:"inherit",onClick:()=>s(!0),variant:"text",children:"About"})}),t.jsxs(M,{onClose:()=>s(!1),open:e,children:[t.jsxs("div",{children:[t.jsx(u,{component:"h2",variant:"h4",children:"About"}),t.jsx(u,{component:"p",variant:"subtitle1",children:"The original idea for Locust was Carl Byström's who made a first proof of concept in June 2010. Jonatan Heyman picked up Locust in January 2011, implemented the current concept of Locust classes and made it work distributed across multiple machines."}),t.jsx(u,{component:"p",sx:{mt:2},variant:"subtitle1",children:"Jonatan, Carl and Joakim Hamrén has continued the development of Locust at their job, ESN Social Software, who have adopted Locust as an inhouse Open Source project."})]}),t.jsxs("div",{children:[t.jsx(u,{component:"h2",variant:"h4",children:"Authors and Copyright"}),t.jsx(m,{sx:{display:"flex",flexDirection:"column",rowGap:.5},children:Ye.map(({name:r,website:o,social:{handle:a,link:i}},c)=>t.jsxs("div",{children:[o?t.jsx(p,{href:o,children:r}):r,t.jsxs(m,{sx:{display:"inline",ml:.5},children:["(",t.jsx(p,{href:i,children:a}),")"]})]},`author-${c}`))})]}),t.jsxs("div",{children:[t.jsx(u,{component:"h2",variant:"h4",children:"License"}),t.jsx(u,{component:"p",variant:"subtitle1",children:"Open source licensed under the MIT license."})]}),t.jsxs("div",{children:[t.jsx(u,{component:"h2",variant:"h4",children:"Version"}),t.jsx(p,{href:`https://github.com/locustio/locust/releases/tag/${n}`,children:n})]}),t.jsxs("div",{children:[t.jsx(u,{component:"h2",variant:"h4",children:"Website"}),t.jsx(p,{href:"https://locust.io/",children:"https://locust.io"})]})]})]})}function Xe(){return t.jsx(m,{component:"nav",sx:{position:"fixed",bottom:0,width:"100%"},children:t.jsx(v,{maxWidth:"xl",sx:{display:"flex",justifyContent:"flex-end"},children:t.jsx(Ze,{})})})}const et={isDarkMode:!1},te=$({name:"theme",initialState:et,reducers:{setIsDarkMode:(e,{payload:s})=>({...e,isDarkMode:s})}}),tt=te.actions,st=te.reducer,k={DARK:"dark",LIGHT:"light"},nt=e=>ke({palette:{mode:e,primary:{main:"#15803d"},success:{main:"#00C853"}}});function rt(){const e=D(({theme:{isDarkMode:r}})=>r),s=C(tt.setIsDarkMode);l.useEffect(()=>{s(localStorage.theme===k.DARK||!("theme"in localStorage)&&window.matchMedia("(prefers-color-scheme: dark)").matches)},[]);const n=()=>{localStorage.theme=e?k.LIGHT:k.DARK,s(!e)};return t.jsx(Q,{color:"inherit",onClick:n,children:e?t.jsx(ve,{}):t.jsx(Re,{})})}const h={READY:"ready",RUNNING:"running",STOPPED:"stopped",SPAWNING:"spawning",CLEANUP:"cleanup",STOPPING:"stopping",MISSING:"missing"};function ot({isDistributed:e,state:s,host:n,totalRps:r,failRatio:o,userCount:a,workerCount:i}){return t.jsxs(m,{sx:{display:"flex",columnGap:2},children:[t.jsxs(m,{sx:{display:"flex",flexDirection:"column"},children:[t.jsx(u,{variant:"button",children:"Host"}),t.jsx(u,{children:n})]}),t.jsx(T,{flexItem:!0,orientation:"vertical"}),t.jsxs(m,{sx:{display:"flex",flexDirection:"column"},children:[t.jsx(u,{variant:"button",children:"Status"}),t.jsx(u,{variant:"button",children:s})]}),s===h.RUNNING&&t.jsxs(t.Fragment,{children:[t.jsx(T,{flexItem:!0,orientation:"vertical"}),t.jsxs(m,{sx:{display:"flex",flexDirection:"column",alignItems:"center"},children:[t.jsx(u,{variant:"button",children:"Users"}),t.jsx(u,{variant:"button",children:a})]})]}),e&&t.jsxs(t.Fragment,{children:[t.jsx(T,{flexItem:!0,orientation:"vertical"}),t.jsxs(m,{sx:{display:"flex",flexDirection:"column",alignItems:"center"},children:[t.jsx(u,{variant:"button",children:"Workers"}),t.jsx(u,{variant:"button",children:i})]})]}),t.jsx(T,{flexItem:!0,orientation:"vertical"}),t.jsxs(m,{sx:{display:"flex",flexDirection:"column",alignItems:"center"},children:[t.jsx(u,{variant:"button",children:"RPS"}),t.jsx(u,{variant:"button",children:r})]}),t.jsx(T,{flexItem:!0,orientation:"vertical"}),t.jsxs(m,{sx:{display:"flex",flexDirection:"column",alignItems:"center"},children:[t.jsx(u,{variant:"button",children:"Failures"}),t.jsx(u,{variant:"button",children:`${o}%`})]})]})}const at=({swarm:{isDistributed:e,state:s,host:n,workerCount:r},ui:{totalRps:o,failRatio:a,userCount:i}})=>({isDistributed:e,state:s,host:n,totalRps:o,failRatio:a,userCount:i,workerCount:r}),it=x(at)(ot),ct=e=>!Object.keys(e).length;function lt(e,s){return{...e,...s}}const ut=e=>{const s=new URLSearchParams;for(const[n,r]of Object.entries(e))if(Array.isArray(r))for(const o of r)s.append(n,o);else s.append(n,r);return s},U=(e,s)=>Object.entries(s).reduce((n,[r,o])=>({...n,[r]:[...n[r]||[],o]}),e);function se(e,s,n){return s&&(Array.isArray(s)?s.map(r=>se(e,r,n)):typeof s=="object"?F(s,n):s)}const F=(e,s)=>Object.entries(e).reduce((n,[r,o])=>({...n,[s(r)]:se(e,o,s)}),{}),ne=e=>e.replace(/_([a-z0-9])/g,(s,n)=>n.toUpperCase()),re=e=>e[0]===e[0].toUpperCase()?e:e.replace(/([a-z0-9])([A-Z0-9])/g,"$1_$2").toLowerCase(),O=e=>F(e,ne),J=e=>F(e,re),dt=e=>e.replace(/([a-z0-9])([A-Z0-9])/g,"$1 $2").replace(/^./,s=>s.toUpperCase()),oe=(e,{shouldTransformKeys:s=!0}={})=>e?Object.entries(e).reduce((n,[r,o])=>{if(!o)return n;const a=s?re(r):r,i=encodeURI(String(o).replace("#",""));return n?`${n}&${a}=${i}`:`?${a}=${i}`},""):"",mt=e=>e.substring(1).split("&").reduce((s,n)=>{const[r,o]=n.split("=");return{...s,[r]:o}},{}),G={GET:"GET",POST:"POST",PUT:"PUT",DELETE:"DELETE"},ht=({body:e,form:s})=>e?s?ut(J(e)):JSON.stringify(J(e)):null,xt=({method:e,body:s,form:n})=>({headers:{"Content-Type":n?"application/x-www-form-urlencoded":"application/json"},method:e,body:ht({body:s,form:n})});async function S(e,{method:s=G.GET,body:n=null,query:r=null,form:o=!1}={}){try{const a=oe(r),i=xt({method:s,body:n,form:o}),c=await fetch(`${e}${a}`,i),d=O(await c.json());if(d.statusCode>=400)throw new Error(`Network Error: Status ${d.statusCode} ${d.message}`);return d}catch(a){return console.error("Network Error:",a),a}}function ae({children:e,onSubmit:s}){const n=l.useCallback(async r=>{r.preventDefault();const o=new FormData(r.target),a={};for(const[i,c]of o.entries())a.hasOwnProperty(i)?(Array.isArray(a[i])||(a[i]=[a[i]]),a[i].push(c)):a[i]=c;s(a)},[s]);return t.jsx("form",{onSubmit:n,children:e})}function pt({onSubmit:e,spawnRate:s,userCount:n}){const r=o=>{e(),S("swarm",{method:G.POST,body:o,form:!0})};return t.jsxs(v,{maxWidth:"md",sx:{my:2},children:[t.jsx(u,{component:"h2",noWrap:!0,variant:"h6",children:"Edit running load test"}),t.jsx(ae,{onSubmit:r,children:t.jsxs(m,{sx:{my:2,display:"flex",flexDirection:"column",rowGap:4},children:[t.jsx(b,{defaultValue:n||1,label:"Number of users (peak concurrency)",name:"userCount"}),t.jsx(b,{defaultValue:s||1,label:"Ramp Up (users started/second)",name:"spawnRate"}),t.jsx(w,{size:"large",type:"submit",variant:"contained",children:"Update Swarm"})]})})]})}const ft=({swarm:{spawnRate:e,userCount:s}})=>({spawnRate:e,userCount:s}),jt=x(ft)(pt);function yt(){const[e,s]=l.useState(!1);return t.jsxs(t.Fragment,{children:[t.jsx(w,{color:"secondary",onClick:()=>s(!0),type:"button",variant:"contained",children:"Edit"}),t.jsx(M,{onClose:()=>s(!1),open:e,children:t.jsx(jt,{onSubmit:()=>s(!1)})})]})}function I({label:e,name:s,options:n,multiple:r=!1,defaultValue:o,sx:a}){return t.jsxs($e,{sx:a,children:[t.jsx(De,{htmlFor:s,shrink:!0,children:e}),t.jsx(Ee,{defaultValue:o||r&&n||n[0],label:e,multiple:r,name:s,native:!0,children:n.map((i,c)=>t.jsx("option",{value:i,children:i},`option-${i}-${c}`))})]})}const z=e=>e.defaultValue!==null&&typeof e.defaultValue!="boolean";function bt({label:e,defaultValue:s,choices:n,helpText:r,isSecret:o}){const a=dt(e),i=r?`${a} (${r})`:a;return n?t.jsx(I,{defaultValue:s,label:i,name:e,options:n,sx:{width:"100%"}}):t.jsx(b,{label:i,name:e,sx:{width:"100%"},type:o?"password":"text",value:s})}function St({extraOptions:e}){const s=l.useMemo(()=>Object.entries(e).reduce((r,[o,a])=>z(a)?[...r,{label:o,...a}]:r,[]),[e]),n=l.useMemo(()=>Object.keys(e).reduce((r,o)=>z(e[o])?r:[...r,o],[]),[e]);return t.jsxs(Y,{children:[t.jsx(Z,{expandIcon:t.jsx(X,{}),children:t.jsx(u,{children:"Custom parameters"})}),t.jsx(ee,{children:t.jsxs(m,{sx:{display:"flex",flexDirection:"column",rowGap:4},children:[s.map((r,o)=>t.jsx(bt,{...r},`valid-parameter-${o}`)),t.jsx(m,{children:n&&t.jsxs(t.Fragment,{children:[t.jsx(u,{children:"The following custom parameters can't be set in the Web UI, because it is a boolean or None type:"}),t.jsx("ul",{children:n.map((r,o)=>t.jsx("li",{children:t.jsx(u,{children:r})},`invalid-parameter-${o}`))})]})})]})})]})}function W(e,{payload:s}){return lt(e,s)}const wt=O(window.templateArgs),ie=$({name:"swarm",initialState:wt,reducers:{setSwarm:W}}),ce=ie.actions,gt=ie.reducer;function Tt({availableShapeClasses:e,availableUserClasses:s,host:n,extraOptions:r,isShape:o,overrideHostWarning:a,setSwarm:i,showUserclassPicker:c,spawnRate:d,userCount:j}){const y=f=>{i({state:h.RUNNING}),S("swarm",{method:G.POST,body:f,form:!0})};return t.jsxs(v,{maxWidth:"md",sx:{my:2},children:[t.jsx(u,{component:"h2",noWrap:!0,variant:"h6",children:"Start new load test"}),t.jsx(ae,{onSubmit:y,children:t.jsxs(m,{sx:{my:2,display:"flex",flexDirection:"column",rowGap:4},children:[c&&t.jsxs(t.Fragment,{children:[t.jsx(I,{label:"User Classes",multiple:!0,name:"userClasses",options:s}),t.jsx(I,{label:"Shape Class",name:"shapeClass",options:e})]}),t.jsx(b,{defaultValue:o&&"-"||j||1,disabled:!!o,label:"Number of users (peak concurrency)",name:"userCount"}),t.jsx(b,{defaultValue:o&&"-"||d||1,disabled:!!o,label:"Ramp Up (users started/second)",name:"spawnRate",title:"Disabled for tests using LoadTestShape class"}),t.jsx(b,{defaultValue:n,label:`Host ${a?"(setting this will override the host for the User classes)":""}`,name:"host",title:"Disabled for tests using LoadTestShape class"}),t.jsxs(Y,{children:[t.jsx(Z,{expandIcon:t.jsx(X,{}),children:t.jsx(u,{children:"Advanced options"})}),t.jsx(ee,{children:t.jsx(b,{label:"Run time (e.g. 20, 20s, 3m, 2h, 1h20m, 3h30m10s, etc.)",name:"runTime",sx:{width:"100%"}})})]}),!ct(r)&&t.jsx(St,{extraOptions:r}),t.jsx(w,{size:"large",type:"submit",variant:"contained",children:"Start Swarm"})]})})]})}const Ct=({swarm:{availableShapeClasses:e,availableUserClasses:s,extraOptions:n,isShape:r,host:o,numUsers:a,overrideHostWarning:i,spawnRate:c,showUserclassPicker:d,userCount:j}})=>({availableShapeClasses:e,availableUserClasses:s,extraOptions:n,isShape:r,host:o,overrideHostWarning:i,showUserclassPicker:d,numUsers:a,spawnRate:c,userCount:j}),kt={setSwarm:ce.setSwarm},le=x(Ct,kt)(Tt);function vt(){const[e,s]=l.useState(!1);return t.jsxs(t.Fragment,{children:[t.jsx(w,{color:"success",onClick:()=>s(!0),type:"button",variant:"contained",children:"New"}),t.jsx(M,{onClose:()=>s(!1),open:e,children:t.jsx(le,{})})]})}function Rt(){const e=()=>{S("stats/reset")};return t.jsx(w,{color:"warning",onClick:e,type:"button",variant:"contained",children:"Reset"})}function $t(){const[e,s]=l.useState(!1);l.useEffect(()=>{s(!1)},[]);const n=()=>{S("stop"),s(!0)};return t.jsx(w,{color:"error",disabled:e,onClick:n,type:"button",variant:"contained",children:e?"Loading":"Stop"})}function Dt(){const e=D(({swarm:s})=>s.state);return e===h.READY?null:e===h.RUNNING?t.jsxs(m,{sx:{display:"flex",columnGap:2},children:[t.jsx(yt,{}),t.jsx($t,{}),t.jsx(Rt,{})]}):t.jsx(vt,{})}function Et(){return t.jsx(Pe,{position:"static",children:t.jsx(v,{maxWidth:"xl",children:t.jsxs(Ae,{sx:{display:"flex",justifyContent:"space-between"},children:[t.jsxs(p,{color:"inherit",href:"#",sx:{display:"flex",alignItems:"center",columnGap:2},underline:"none",children:[t.jsx("img",{height:"52",src:"/assets/logo.png",width:"52"}),t.jsx(u,{component:"h1",noWrap:!0,sx:{fontWeight:700,display:"flex",alignItems:"center"},variant:"h3",children:"Locust"})]}),t.jsxs(m,{sx:{display:"flex",columnGap:6},children:[t.jsx(it,{}),t.jsx(Dt,{}),t.jsx(rt,{})]})]})})})}function Pt({children:e}){return t.jsxs(t.Fragment,{children:[t.jsx(Et,{}),t.jsx("main",{children:e}),t.jsx(Xe,{})]})}const N=(e,s=0)=>{const n=Math.pow(10,s);return Math.round(e*n)/n};function R({rows:e,structure:s}){return t.jsx(Ie,{component:Ne,children:t.jsxs(Le,{children:[t.jsx(Me,{children:t.jsx(K,{children:s.map(({title:n,key:r})=>t.jsx(q,{children:n},`table-head-${r}`))})}),t.jsx(Ue,{children:e.map((n,r)=>t.jsx(K,{children:s.map(({key:o,round:a},i)=>t.jsx(q,{children:a?N(n[o],a):n[o]},`table-row=${i}`))},`${n.name}-${r}`))})]})})}function At({rows:e,tableStructure:s}){return s?t.jsx(R,{rows:e,structure:s}):null}const It=({swarm:{extendedTables:e},ui:{extendedStats:s},url:{query:n}})=>{const r=n&&n.tab&&e&&e.find(({key:a})=>a===n.tab),o=n&&n.tab&&s&&s.find(({key:a})=>a===n.tab);return{tableStructure:r?r.structure.map(({key:a,...i})=>({key:ne(a),...i})):null,rows:o?o.data:[]}},Nt=x(It)(At),Lt=[{key:"count",title:"# occurrences"},{key:"traceback",title:"Traceback"}];function Mt({exceptions:e}){return t.jsx(R,{rows:e,structure:Lt})}const Ut=({ui:{exceptions:e}})=>({exceptions:e}),Ft=x(Ut)(Mt),Ot=[{key:"occurrences",title:"# Failures"},{key:"method",title:"Method"},{key:"name",title:"Name"},{key:"error",title:"Message"}];function Gt({errors:e}){return t.jsx(R,{rows:e,structure:Ot})}const Wt=({ui:{errors:e}})=>({errors:e}),Ht=x(Wt)(Gt);function Bt({extendedCsvFiles:e,statsHistoryEnabled:s}){return t.jsxs(Fe,{sx:{display:"flex",flexDirection:"column"},children:[t.jsx(g,{children:t.jsx(p,{href:"/stats/requests/csv",children:"Download requests CSV"})}),s&&t.jsx(g,{children:t.jsx(p,{href:"/stats/requests_full_history/csv",children:"Download full request statistics history CSV"})}),t.jsx(g,{children:t.jsx(p,{href:"/stats/failures/csv",children:"Download failures CSV"})}),t.jsx(g,{children:t.jsx(p,{href:"/exceptions/csv",children:"Download exceptions CSV"})}),t.jsx(g,{children:t.jsx(p,{href:"/stats/report",target:"_blank",children:"Download Report"})}),e&&e.map(({href:n,title:r})=>t.jsx(g,{children:t.jsx(p,{href:n,children:r})}))]})}const _t=({swarm:{extendedCsvFiles:e,statsHistoryEnabled:s}})=>({extendedCsvFiles:e,statsHistoryEnabled:s}),Vt=x(_t)(Bt),Kt=[{key:"method",title:"Type"},{key:"name",title:"Name"},{key:"numRequests",title:"# Requests"},{key:"numFailures",title:"# Fails"},{key:"medianResponseTime",title:"Median (ms)",round:2},{key:"ninetiethResponseTime",title:"90%ile (ms)"},{key:"ninetyNinthResponseTime",title:"99%ile (ms)"},{key:"avgResponseTime",title:"Average (ms)",round:2},{key:"minResponseTime",title:"Min (ms)"},{key:"maxResponseTime",title:"Max (ms)"},{key:"avgContentLength",title:"Average size (bytes)",round:2},{key:"currentRps",title:"Current RPS",round:2},{key:"currentFailPerSec",title:"Current Failures/s"}];function qt({stats:e}){return t.jsx(R,{rows:e,structure:Kt})}const Jt=({ui:{stats:e}})=>({stats:e}),zt=x(Jt)(qt),Qt=({charts:e,title:s,seriesData:n})=>({legend:{icon:"circle",inactiveColor:"#b3c3bc",textStyle:{color:"#b3c3bc"}},title:{text:s,x:10,y:10},tooltip:{trigger:"axis",formatter:r=>r&&Array.isArray(r)&&r.length>0&&r.some(o=>!!o.value)?r.reduce((o,{color:a,seriesName:i,value:c})=>` - ${o} -
- - ${i}: ${c} - - `,""):"No data",axisPointer:{animation:!0},textStyle:{color:"#b3c3bc",fontSize:13},backgroundColor:"rgba(21,35,28, 0.93)",borderWidth:0,extraCssText:"z-index:1;"},xAxis:{type:"category",splitLine:{show:!1},axisLine:{lineStyle:{color:"#5b6f66"}},data:e.time},yAxis:{type:"value",boundaryGap:[0,"5%"],splitLine:{show:!1},axisLine:{lineStyle:{color:"#5b6f66"}}},series:n,grid:{x:60,y:70,x2:40,y2:40},color:["#00ca5a","#ff6d6d"],toolbox:{feature:{saveAsImage:{name:s.replace(/\s+/g,"_").toLowerCase()+"_"+new Date().getTime()/1e3,title:"Download as PNG",emphasis:{iconStyle:{textPosition:"left"}}}}}}),Yt=({charts:e,lines:s})=>s.map(({key:n,name:r})=>({name:r,type:"line",showSymbol:!0,data:e[n]})),Zt=e=>({symbol:"none",label:{formatter:s=>`Run #${s.dataIndex+1}`},lineStyle:{color:"#5b6f66"},data:(e.markers||[]).map(s=>({xAxis:s}))});Oe("locust",{backgroundColor:"#27272a",xAxis:{lineColor:"#f00"},textStyle:{color:"#b3c3bc"},title:{textStyle:{color:"#b3c3bc"}}});function Xt({charts:e,title:s,lines:n}){const[r,o]=l.useState(null),a=l.useRef(null);return l.useEffect(()=>{if(!a.current)return;const i=Ge(a.current,"locust");return i.setOption(Qt({charts:e,title:s,seriesData:Yt({charts:e,lines:n})})),o(i),()=>{We(i)}},[a]),l.useEffect(()=>{const i=n.every(({key:c})=>!!e[c]);r&&i&&r.setOption({xAxis:{data:e.time},series:n.map(({key:c},d)=>({data:e[c],...d===0?{markLine:Zt(e)}:{}}))})},[e,r,n]),t.jsx("div",{ref:a,style:{width:"100%",height:"300px"}})}const es=({ui:{charts:e}})=>({charts:e}),ts=x(es)(Xt),ss=[{title:"Total Requests per Second",lines:[{name:"RPS",key:"currentRps"},{name:"Failures/s",key:"currentFailPerSec"}]},{title:"Response Times (ms)",lines:[{name:"Median Response Time",key:"responseTimePercentile1"},{name:"95% percentile",key:"responseTimePercentile2"}]},{title:"Number of Users",lines:[{name:'"Number of Users"',key:"userCount"}]}];function ns(){return t.jsx("div",{children:ss.map((e,s)=>t.jsx(ts,{...e},`swarm-chart-${s}`))})}function rs(e){return(e*100).toFixed(1)+"%"}function L({classRatio:e}){return t.jsx("ul",{children:Object.entries(e).map(([s,{ratio:n,tasks:r}])=>t.jsxs("li",{children:[`${rs(n)} ${s}`,r&&t.jsx(L,{classRatio:r})]},`nested-ratio-${s}`))})}function os({ratios:{perClass:e,total:s}}){return!e&&!s?null:t.jsxs("div",{children:[e&&t.jsxs(t.Fragment,{children:[t.jsx("h3",{children:"Ratio Per Class"}),t.jsx(L,{classRatio:e})]}),s&&t.jsxs(t.Fragment,{children:[t.jsx("h3",{children:"Total Ratio"}),t.jsx(L,{classRatio:s})]})]})}const as=({ui:{ratios:e}})=>({ratios:e}),is=x(as)(os),cs=[{key:"id",title:"Worker"},{key:"state",title:"State"},{key:"userCount",title:"# users"},{key:"cpuUsage",title:"CPU usage"},{key:"memoryUsage",title:"Memory usage"}];function ls({workers:e=[]}){return t.jsx(R,{rows:e,structure:cs})}const us=({ui:{workers:e}})=>({workers:e}),ds=x(us)(ls),ms=[{component:zt,key:"stats",title:"Statistics"},{component:ns,key:"charts",title:"Charts"},{component:Ht,key:"failures",title:"Failures"},{component:Ft,key:"exceptions",title:"Exceptions"},{component:is,key:"ratios",title:"Current Ratio"},{component:Vt,key:"reports",title:"Download Data"}],hs=[{component:ds,key:"workers",title:"Workers",shouldDisplayTab:e=>e.swarm.isDistributed}],xs=e=>{const s=new URL(window.location.href),n=`${s.origin}${s.pathname}${oe(e,{shouldTransformKeys:!1})}`;window.history.pushState({path:n},"",n)},ps=()=>window.location.search?mt(window.location.search):null,fs={query:ps()},ue=$({name:"url",initialState:fs,reducers:{setUrl:W}}),js=ue.actions,ys=ue.reducer;function bs({currentTabIndexFromQuery:e,setUrl:s,tabs:n}){const[r,o]=l.useState(e),a=(i,c)=>{const d=n[c].key;xs({tab:d}),s({query:{tab:d}}),o(c)};return t.jsxs(v,{maxWidth:"xl",children:[t.jsx(m,{sx:{mb:2},children:t.jsx(He,{onChange:a,value:r,children:n.map(({title:i},c)=>t.jsx(Be,{label:i},`tab-${c}`))})}),n.map(({component:i=Nt},c)=>r===c&&t.jsx(i,{},`tabpabel-${c}`))]})}const Ss=e=>{const{swarm:{extendedTabs:s=[]},url:{query:n}}=e,r=hs.filter(({shouldDisplayTab:a})=>a(e)),o=[...ms,...r,...s];return{tabs:o,currentTabIndexFromQuery:n&&n.tab?o.findIndex(({key:a})=>a===n.tab):0}},ws={setUrl:js.setUrl},gs=x(Ss,ws)(bs);function E(e,{execute:s=!1}={}){const[n,r]=l.useState(!0),[o,a]=l.useState(null),[i,c]=l.useState(null),d=l.useCallback((...j)=>(a(null),c(null),e(...j).then(y=>{a(y),r(!1)}).catch(y=>{c(y),r(!1)})),[e]);return l.useEffect(()=>{s&&d()},[d,s]),{execute:d,isLoading:n,value:o,error:i}}function P(e,s,{shouldRunInterval:n}={shouldRunInterval:!0}){const r=l.useRef(e);l.useEffect(()=>{r.current=e},[e]),l.useEffect(()=>{if(!n)return;const o=setInterval(()=>r.current(),s);return()=>{clearInterval(o)}},[s,n])}const Ts=e=>e[e.length-1],Cs={totalRps:0,failRatio:0,stats:[],errors:[],exceptions:[],charts:O(window.templateArgs).history.reduce(U,{}),ratios:{},userCount:0},ks=(e,s)=>U(e,{currentRps:{value:null},currentFailPerSec:{value:null},responseTimePercentile1:{value:null},responseTimePercentile2:{value:null},userCount:{value:null},time:s}),de=$({name:"ui",initialState:Cs,reducers:{setUi:W,updateCharts:(e,{payload:s})=>({...e,charts:U(e.charts,s)}),updateChartMarkers:(e,{payload:s})=>({...e,charts:{...ks(e.charts,s.length?Ts(s):e.charts.time[0]),markers:e.charts.markers?[...e.charts.markers,s]:[e.charts.time[0],s]}})}}),A=de.actions,vs=de.reducer;function Rs(){const e=C(ce.setSwarm),s=C(A.setUi),n=C(A.updateCharts),r=C(A.updateChartMarkers),o=D(({swarm:f})=>f),a=l.useRef(o.state),[i,c]=l.useState(!1),{execute:d}=E(async()=>{const{extendedStats:f,state:me,stats:he,errors:xe,totalRps:pe,failRatio:H,workers:fe,currentResponseTimePercentile1:je,currentResponseTimePercentile2:ye,userCount:B}=await S("stats/requests");me===h.STOPPED&&e({state:h.STOPPED});const _=new Date().toLocaleTimeString();i&&(c(!1),r(_));const V=N(pe,2),be=N(H*100),Se={currentRps:V,currentFailPerSec:H,responseTimePercentile1:je,responseTimePercentile2:ye,userCount:B,time:_};s({extendedStats:f,stats:he,errors:xe,totalRps:V,failRatio:be,workers:fe,userCount:B}),n(Se)}),{execute:j}=E(async()=>{const f=await S("tasks");s({ratios:f})}),{execute:y}=E(async()=>{const{exceptions:f}=await S("exceptions");s({exceptions:f})});P(d,2e3,{shouldRunInterval:o.state===h.RUNNING}),P(j,5e3,{shouldRunInterval:o.state===h.RUNNING}),P(y,5e3,{shouldRunInterval:o.state===h.RUNNING}),l.useEffect(()=>{o.state===h.RUNNING&&a.current===h.STOPPED&&c(!0),a.current=o.state},[o.state,a]),l.useEffect(()=>{o.state===h.STOPPED&&(d(),j())},[])}function $s({isDarkMode:e,swarmState:s}){Rs();const n=l.useMemo(()=>nt(e?k.DARK:k.LIGHT),[e]);return t.jsxs(_e,{theme:n,children:[t.jsx(Ve,{}),t.jsx(Pt,{children:s===h.READY?t.jsx(le,{}):t.jsx(gs,{})})]})}const Ds=({swarm:{state:e},theme:{isDarkMode:s}})=>({isDarkMode:s,swarmState:e}),Es=x(Ds)($s),Ps=Ke({swarm:gt,theme:st,ui:vs,url:ys}),As=qe({reducer:Ps}),Is=Je.createRoot(document.getElementById("root"));Is.render(t.jsx(ze,{store:As,children:t.jsx(Es,{})})); diff --git a/locust/webui/dist/index.html b/locust/webui/dist/index.html index f6936763c4..68ee87342f 100644 --- a/locust/webui/dist/index.html +++ b/locust/webui/dist/index.html @@ -13,7 +13,7 @@ Locust - + diff --git a/locust/webui/src/components/SwarmForm/SwarmForm.tsx b/locust/webui/src/components/SwarmForm/SwarmForm.tsx index 162ff6b19c..a80411a540 100644 --- a/locust/webui/src/components/SwarmForm/SwarmForm.tsx +++ b/locust/webui/src/components/SwarmForm/SwarmForm.tsx @@ -40,6 +40,7 @@ interface ISwarmForm | 'isShape' | 'host' | 'overrideHostWarning' + | 'runTime' | 'showUserclassPicker' | 'spawnRate' | 'userCount' @@ -52,6 +53,7 @@ function SwarmForm({ extraOptions, isShape, overrideHostWarning, + runTime, setSwarm, showUserclassPicker, spawnRate, @@ -115,6 +117,7 @@ function SwarmForm({