diff --git a/dashboard/static/dashboard/dist/assets/LabsConsole.js b/dashboard/static/dashboard/dist/assets/LabsConsole.js index 1fa5db1..5b48640 100644 --- a/dashboard/static/dashboard/dist/assets/LabsConsole.js +++ b/dashboard/static/dashboard/dist/assets/LabsConsole.js @@ -1,6 +1,6 @@ -import{At as e,Ci as t,Fr as n,Ht as r,Mr as i,Ot as a,Pr as o,Ur as s,a as c,c as l,ci as u,ir as d,kt as f,l as p,o as m,pn as h,s as g,sr as _,xi as ee,xr as te,yi as v}from"./internal.js";import{$ as ne,A as y,Ai as re,J as b,K as x,M as ie,Mi as ae,Q as S,X as C,Xa as oe,Y as se,Z as w,a as ce,ao as le,i as ue,j as de,ji as fe,ki as pe,l as T,n as me,o as E,q as D,qa as he,r as ge,s as _e,u as ve}from"./app.js";import{t as ye}from"./CodeSnippet.js";import{t as be}from"./tabs.js";import{t as O}from"./column-layout.js";import{t as k}from"./link.js";import{t as xe}from"./radio-group.js";import{t as Se}from"./segmented-control.js";var A=t(ee());function j(e){if(e?.progressBar)return{[d.progressBarBackgroundColor]:e.progressBar.backgroundColor,[d.progressBarBorderRadius]:e.progressBar.borderRadius,[d.progressBarHeight]:e.progressBar.height}}function M(e){if(e?.progressValue)return{[d.progressValueBackgroundColor]:e.progressValue.backgroundColor}}function Ce(e){if(e?.progressPercentage)return{color:e.progressPercentage.color,fontSize:e.progressPercentage.fontSize,fontWeight:e.progressPercentage.fontWeight}}var N={progress:`awsui_progress_11huc_15p30_13`,"result-state":`awsui_result-state_11huc_15p30_27`,"awsui-motion-fade-in":`awsui_awsui-motion-fade-in_11huc_15p30_1`,root:`awsui_root_11huc_15p30_49`,"with-result-button":`awsui_with-result-button_11huc_15p30_81`,"result-text":`awsui_result-text_11huc_15p30_85`,"result-container-error":`awsui_result-container-error_11huc_15p30_89`,"result-container-success":`awsui_result-container-success_11huc_15p30_89`,"result-button":`awsui_result-button_11huc_15p30_93`,"word-wrap":`awsui_word-wrap_11huc_15p30_99`,"label-flash":`awsui_label-flash_11huc_15p30_104`,"label-key-value":`awsui_label-key-value_11huc_15p30_108`,flash:`awsui_flash_11huc_15p30_116`,"progress-container":`awsui_progress-container_11huc_15p30_120`,"percentage-container":`awsui_percentage-container_11huc_15p30_127`,percentage:`awsui_percentage_11huc_15p30_127`,complete:`awsui_complete_11huc_15p30_173`,"progress-flash":`awsui_progress-flash_11huc_15p30_186`,"additional-info":`awsui_additional-info_11huc_15p30_199`},P=100,F=(e,t,n)=>Math.max(Math.min(e,n),t),I=({value:e,isInFlash:t,ariaLabel:n,ariaLabelledby:r,ariaDescribedby:a,style:o})=>{let s=F(Math.round(e),0,P),c=j(o),l=M(o),u=Ce(o);return A.createElement(`div`,{className:N[`progress-container`]},A.createElement(`progress`,{className:i(N.progress,s>=P&&N.complete,t&&N[`progress-flash`]),max:P,value:s,"aria-label":n,"aria-labelledby":n?void 0:r,"aria-describedby":a,style:{...c||{},...l||{}}}),A.createElement(`span`,{"aria-hidden":`true`,className:N[`percentage-container`]},A.createElement(f,{className:N.percentage,variant:`small`,color:t?`inherit`:void 0,nativeAttributes:u?{style:u}:void 0},`${s}%`)))},L=({color:e,children:t,className:n,id:r})=>A.createElement(f,{className:i(N[`word-wrap`],n),variant:`small`,display:`block`,color:e,id:r},t),R=({onClick:e,children:t})=>A.createElement(`div`,{className:N[`result-button`]},A.createElement(r,{formAction:`none`,onClick:e},t)),we=({isInFlash:t,resultText:n,resultButtonText:r,status:a,onClick:o})=>{let s=!!r;return t?A.createElement(`div`,{className:N[`result-container-${a}`]},A.createElement(`span`,{className:N[`result-text`]},n)):A.createElement(`div`,{className:N[`result-container-${a}`]},A.createElement(`span`,{className:i(s&&N[`with-result-button`])},A.createElement(e,{type:a===`success`?`success`:`error`},A.createElement(`span`,{className:N[`result-text`]},n))),s&&A.createElement(R,{onClick:o},r))},Te=5e3;function Ee({value:e=0,status:t=`in-progress`,variant:r=`standalone`,resultButtonText:o,label:c,ariaLabel:l,ariaLabelledby:d,ariaDescribedby:f,description:p,additionalInfo:m,resultText:g,onResultButtonClick:ee,style:v,...ne}){let{__internalRootRef:y}=n(`ProgressBar`,{props:{variant:r}}),re=te(ne),b=`${s(`awsui-progress-bar-`)}-label`,x=r===`flash`,ae=t===`in-progress`,S=s(`progressbar-description-`),C=s(`progressbar-additional-info-`),[oe,se]=(0,A.useState)(``),w=ie(e=>{se(`${e}%`)},Te,[]);return(0,A.useEffect)(()=>(w(e),()=>w.cancel()),[w,e]),x&&o&&u(`ProgressBar`,'The `resultButtonText` is ignored if you set `variant="flash"`, and the result button is not displayed. Use the `buttonText` property and the `onButtonClick` event listener of the flashbar item in which the progress bar component is embedded.'),A.createElement(`div`,{...re,className:i(re.className,N.root,r&&N[r]),ref:y},A.createElement(`div`,{className:x?N[`flash-container`]:void 0},A.createElement(`div`,{className:i(N[`word-wrap`],N[`label-${r}`]),id:b},c),p&&A.createElement(L,{color:x?`inherit`:void 0,id:S},p),A.createElement(`div`,null,ae?A.createElement(A.Fragment,null,A.createElement(I,{value:e,ariaLabel:l,ariaLabelledby:a(b,d),ariaDescribedby:a(p?S:void 0,m?C:void 0,f),isInFlash:x,style:v}),A.createElement(h,{hidden:!0,tagName:`span`,delay:0},c,c?`: `:null,oe)):A.createElement(h,{hidden:!1,tagName:`span`,delay:0},A.createElement(we,{resultText:g,isInFlash:x,resultButtonText:o,status:t,onClick:()=>{_(ee)}})))),m&&A.createElement(L,{className:N[`additional-info`],color:x?`inherit`:void 0,id:C},m))}o(Ee,`ProgressBar`);var z=v(),De=`floci_quiz_scores`,Oe=`floci:quiz-scores-changed`;function B(){try{let e=localStorage.getItem(De);return e?JSON.parse(e):{}}catch{return{}}}function ke(e,t,n){let r=B(),i=r[e];if(i&&i.total===n&&i.correct>=t)return;let a={...r,[e]:{correct:t,total:n}};try{localStorage.setItem(De,JSON.stringify(a))}catch{}window.dispatchEvent(new CustomEvent(Oe,{detail:a}))}function V(){let[e,t]=(0,A.useState)(B);return(0,A.useEffect)(()=>{let e=e=>t(e.detail);return window.addEventListener(Oe,e),()=>window.removeEventListener(Oe,e)},[]),e}var Ae=e=>!!(e&&e.total>0&&e.correct/e.total>=.7),H=(e,t)=>e.length===t.length&&t.every(t=>e.includes(t)),U=[``,`ONE`,`TWO`,`THREE`,`FOUR`],W=({quizId:e,questions:t,best:n})=>{let[r,i]=(0,A.useState)(()=>t.map(()=>[])),[a,o]=(0,A.useState)(!1),s=t.filter((e,t)=>H(r[t],e.answers)).length,l=t.every((e,t)=>r[t].length===e.answers.length),u=(e,t,n,r)=>{i(i=>i.map((i,a)=>a===e?n?r?[...i.filter(e=>e!==t),t]:i.filter(e=>e!==t):[t]:i))},d=()=>{o(!0),ke(e,s,t.length)},f=()=>{i(t.map(()=>[])),o(!1)},p=n?` · best ${n.correct}/${n.total}`:``;return(0,z.jsx)(de,{headerText:`Practice quiz: ${t.length} questions${p}`,variant:`footer`,children:(0,z.jsx)(`div`,{"data-testid":`quiz-${e}`,children:(0,z.jsxs)(g,{size:`m`,children:[t.map((e,t)=>{let n=e.answers.length>1,i=r[t],o=H(i,e.answers),s=e.options.map((e,t)=>({value:String(t),label:e}));return(0,z.jsx)(`div`,{"data-testid":`quiz-question-${t+1}`,children:(0,z.jsxs)(g,{size:`xxs`,children:[(0,z.jsxs)(m,{variant:`strong`,children:[t+1,`. `,e.question]}),n?(0,z.jsxs)(g,{size:`xxs`,children:[e.options.map((n,r)=>(0,z.jsx)(me,{checked:i.includes(r),disabled:a||!i.includes(r)&&i.length>=e.answers.length,onChange:({detail:e})=>u(t,r,!0,e.checked),children:n},n)),!e.question.includes(`(Choose`)&&(0,z.jsxs)(m,{variant:`small`,children:[`Choose `,U[e.answers.length]||e.answers.length,`.`]})]}):(0,z.jsx)(xe,{value:i.length?String(i[0]):null,items:s.map(e=>({...e,disabled:a})),onChange:({detail:e})=>u(t,Number(e.value),!1,!0)}),a&&(0,z.jsxs)(g,{size:`xxs`,children:[(0,z.jsx)(T,{type:o?`success`:`error`,children:o?`Correct`:`Answer: ${e.answers.map(t=>e.options[t]).join(`; `)}`}),(0,z.jsx)(m,{variant:`small`,color:`text-body-secondary`,children:e.explanation})]})]})},e.question)}),(0,z.jsx)(g,{direction:`horizontal`,size:`xs`,alignItems:`center`,children:a?(0,z.jsxs)(z.Fragment,{children:[(0,z.jsxs)(T,{type:Ae({correct:s,total:t.length})?`success`:`warning`,children:[s,` of `,t.length,` correct`]}),(0,z.jsx)(c,{onClick:f,children:`Try again`})]}):(0,z.jsx)(c,{variant:`primary`,disabled:!l,onClick:d,children:`Check answers`})})]})})})};function je(e){return`${e.exam.split(`-`)[0]} ${e.task}`}var G=(e,t)=>`${e}:${t}`;function K(e){return[...new Set(e.tasks.flatMap(e=>e.labs.map(e=>G(e.service,e.key))))]}function Me(e,t){let n=0,r=0,i=0;for(let a of e.domains){let e=K(a);e.length>0&&(n+=a.weight*(e.filter(e=>t[e]).length/e.length)),i+=a.tasks.length,r+=a.tasks.filter(e=>e.labs.length>0).length}return{weighted:Math.round(n),tasksWithLabs:r,taskCount:i}}var q=({service:e,labKey:t,title:n,primary:r,completed:i,onOpenLab:a})=>{let o=w(e,t),s=!!i[G(e,t)];return(0,z.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`6px`,flexWrap:`wrap`},children:[s?(0,z.jsx)(T,{type:`success`}):(0,z.jsx)(T,{type:`pending`}),o&&(0,z.jsxs)(E,{color:`grey`,children:[`#`,o.id]}),(0,z.jsx)(E,{color:`blue`,children:e.toUpperCase()}),(0,z.jsx)(c,{variant:`inline-link`,onClick:()=>a(e,t),children:r?(0,z.jsx)(`strong`,{children:n}):n})]})},J=({card:e,completed:t,onOpenLab:n})=>(0,z.jsx)(de,{headerText:`Concept: ${e.title}`,variant:`footer`,children:(0,z.jsxs)(g,{size:`xs`,children:[(0,z.jsx)(`ul`,{style:{margin:0,paddingLeft:`20px`},children:e.points.map(e=>(0,z.jsx)(`li`,{children:e},e))}),e.related_labs.length>0&&(0,z.jsxs)(m,{variant:`small`,children:[`Related labs:`,` `,e.related_labs.map((e,r)=>{let i=w(e.service,e.key);return(0,z.jsxs)(A.Fragment,{children:[r>0&&`, `,(0,z.jsx)(c,{variant:`inline-link`,onClick:()=>n(e.service,e.key),children:i?`#${i.id} ${i.title}`:`${e.service}/${e.key}`}),t[G(e.service,e.key)]?` ✓`:``]},G(e.service,e.key))})]}),e.planned&&(0,z.jsxs)(m,{variant:`small`,color:`text-body-secondary`,children:[`Planned lab: `,e.planned]})]})}),Ne=({exam:e,concepts:t,completed:n,onOpenLab:r})=>{let i=Me(e,n),a=V(),o=e.domains.flatMap(e=>e.tasks).filter(e=>(e.quiz?.length??0)>0),s=o.filter(t=>Ae(a[`${e.code}:${t.id}`])).length;return(0,z.jsxs)(g,{size:`m`,children:[(0,z.jsxs)(O,{columns:2,variant:`text-grid`,children:[(0,z.jsxs)(g,{size:`xxs`,children:[(0,z.jsxs)(m,{variant:`h2`,children:[e.code,` · `,e.title]}),(0,z.jsxs)(m,{variant:`small`,color:`text-body-secondary`,children:[`Domains and task statements from the`,` `,(0,z.jsx)(k,{href:e.guide_url,external:!0,children:`official exam guide`}),` `,`(checked `,e.checked,`). Labs show the tasks they practice; concept cards cover objectives that can't be practiced in a local emulator.`]})]}),(0,z.jsxs)(g,{size:`xs`,children:[(0,z.jsx)(Ee,{value:i.weighted,label:`Exam-weighted lab progress`,description:`Completed labs, weighted by each domain's share of the exam. Hands-on labs cover ${i.tasksWithLabs} of ${i.taskCount} task statements; the rest have concept cards.`,status:i.weighted===100?`success`:`in-progress`}),o.length>0&&(0,z.jsx)(`div`,{"data-testid":`quiz-progress-${e.code}`,children:(0,z.jsxs)(m,{variant:`small`,color:`text-body-secondary`,children:[`Practice quizzes passed: `,s,` of `,o.length,` task statements (70% or better).`]})})]})]}),e.domains.map((i,o)=>{let s=K(i),c=s.filter(e=>n[e]).length;return(0,z.jsx)(de,{variant:`container`,defaultExpanded:o===0,headerText:`Domain ${i.id}: ${i.title}`,headerDescription:`${i.weight}% of scored content · ${s.length>0?`${c} of ${s.length} labs complete`:`no hands-on labs yet`}`,headerActions:(0,z.jsxs)(E,{color:s.length>0?`blue`:`grey`,children:[i.weight,`%`]}),children:(0,z.jsx)(g,{size:`l`,children:i.tasks.map(i=>(0,z.jsx)(`div`,{"data-testid":`exam-task-${e.code}-${i.id}`,children:(0,z.jsxs)(g,{size:`xs`,children:[(0,z.jsxs)(m,{variant:`h4`,children:[i.id,` `,i.title]}),i.labs.length>0?(0,z.jsx)(g,{size:`xxs`,children:i.labs.map(e=>(0,z.jsx)(q,{service:e.service,labKey:e.key,title:e.title,primary:e.primary,completed:n,onOpenLab:r},G(e.service,e.key)))}):(0,z.jsx)(T,{type:`warning`,children:`No hands-on lab yet`}),i.concepts.map(e=>t[e]).filter(Boolean).map(e=>(0,z.jsx)(J,{card:e,completed:n,onOpenLab:r},e.key)),i.quiz&&i.quiz.length>0&&(0,z.jsx)(W,{quizId:`${e.code}:${i.id}`,questions:i.quiz,best:a[`${e.code}:${i.id}`]})]})},i.id))})},i.id)}),(0,z.jsx)(m,{variant:`small`,color:`text-body-secondary`,children:`Bold labs practice this task statement as their main focus; the rest touch it along the way.`})]})},Pe=`floci_labs_study_track`,Y=`levels`;function Fe(){let e=new URLSearchParams(window.location.search).get(`track`);if(e)return e;try{return localStorage.getItem(Pe)||Y}catch{return Y}}function Ie(e=[]){return[...e].sort((e,t)=>Number(t.primary)-Number(e.primary)||e.exam.localeCompare(t.exam)||e.task.localeCompare(t.task,void 0,{numeric:!0}))}var Le=()=>{let[e,t]=(0,A.useState)([]),[n,r]=(0,A.useState)(null),[i,a]=(0,A.useState)(null),[o,s]=(0,A.useState)(Fe),u=e=>{s(e);try{localStorage.setItem(Pe,e)}catch{}let t=new URL(window.location.href);e===Y?t.searchParams.delete(`track`):t.searchParams.set(`track`,e),window.history.replaceState(null,``,t.pathname+t.search)},[d,f]=(0,A.useState)(ue);(0,A.useEffect)(()=>{ce(d)},[d]);let h=(e,t,n)=>{f(r=>({...r,[`${e}:${t}`]:n}))},[_,ee]=(0,A.useState)(null),[te,v]=(0,A.useState)(!0),[ie,me]=(0,A.useState)(null),[O,k]=(0,A.useState)(!1),[xe,j]=(0,A.useState)(null),[M,Ce]=(0,A.useState)(``),[N,P]=(0,A.useState)(`all`),[F,I]=(0,A.useState)(null),[L,R]=(0,A.useState)(!1),[we,Te]=(0,A.useState)(()=>{try{let e=localStorage.getItem(`floci_labs_active_sdk`);if(e===`cli`||e===`boto3`||e===`terraform`)return e}catch{}return`cli`}),De=e=>{Te(e);try{localStorage.setItem(`floci_labs_active_sdk`,e)}catch{}},[Oe,B]=(0,A.useState)({}),[ke,V]=(0,A.useState)({}),[Ae,H]=(0,A.useState)({}),U=(0,A.useRef)(!1),W=(0,A.useRef)(0),G=(0,A.useRef)({}),[K,Me]=(0,A.useState)(null),q=async()=>{v(!0),me(null);try{let e=((await fe()).services||[]).flatMap(e=>e.labs);t(e),a(null),pe().then(r).catch(e=>a(e?.message||`Could not load the exam study tracks.`));let n=new URLSearchParams(window.location.search),i=n.get(`lab`),o=n.get(`service`);if(i){let t=e.find(e=>e.key===i&&(!o||e.service===o));t&&J(t,!1)}ae().then(e=>{f(t=>{let n={...t};return(e.labs||[]).forEach(e=>{let t=e.key||e.lab;t&&(n[`${e.service}:${t}`]=!!e.complete)}),n})})}catch(e){console.error(e),me(e?.message||`Could not load the lab catalog.`)}finally{v(!1)}};(0,A.useEffect)(()=>{q()},[]),(0,A.useEffect)(()=>{let t=()=>{let t=new URLSearchParams(window.location.search),n=t.get(`lab`),r=t.get(`service`);if(n&&e.length>0){let t=e.find(e=>e.key===n&&(!r||e.service===r));t&&J(t,!1)}else!n&&_&&Re(!1)};return window.addEventListener(`popstate`,t),window.addEventListener(`floci_navigate`,t),()=>{window.removeEventListener(`popstate`,t),window.removeEventListener(`floci_navigate`,t)}},[e,_]);let J=async(e,t=!0)=>{U.current=!0;let n=++W.current;if(R(!1),I(null),j(null),ee(e),B({}),V({}),H({}),k(!0),t){let t=new URL(window.location.href);t.searchParams.set(`lab`,e.key),t.searchParams.set(`service`,e.service),t.hash=se,window.history.pushState(null,``,t.pathname+t.search+t.hash),window.dispatchEvent(new Event(`floci_url_changed`))}try{let t=await re(e.service,e.key);if(n!==W.current)return;if(t.error)throw Error(t.error);let r={},i={};if(Object.entries(t.steps||{}).forEach(([e,t])=>{r[e]=!!t.verified,t.verification&&(i[e]={status:t.verified?`Verified`:`Unverified`,body:t.verification.message||JSON.stringify(t.verification,null,2)})}),V(r),B(i),window.location.hash===`#next-step`){let t=e.steps||[],n=t.findIndex(e=>!(r[e.key]||e.status?.verified));Me(n>0?ne(t[n].key):C)}h(e.service,e.key,!!t.complete)}catch(e){n===W.current&&j(e.message||`Failed to load lab status`)}finally{n===W.current&&k(!1)}},Le=_?`${_.service}:${_.key}`:``;(0,A.useLayoutEffect)(()=>{if(!Le)return;let e=(window.location.hash.startsWith(`#step-`)?document.getElementById(window.location.hash.slice(1)):null)||document.getElementById(`lab-top`);e?e.scrollIntoView({block:`start`}):window.scrollTo({top:0})},[Le]),(0,A.useLayoutEffect)(()=>{if(!K||(Me(null),window.location.hash!==`#next-step`))return;let e=document.getElementById(K);e&&e.scrollIntoView({block:`start`}),K!==`lab-top`&&window.history.replaceState(window.history.state,``,`${window.location.pathname}${window.location.search}#${K}`)},[K]);let Re=(e=!0)=>{if(++W.current,U.current=!0,R(!1),ee(null),B({}),V({}),H({}),e){let e=new URL(window.location.href);e.searchParams.delete(`lab`),e.searchParams.delete(`service`),e.hash=``,window.history.pushState(null,``,e.pathname+e.search),window.dispatchEvent(new Event(`floci_url_changed`))}},ze=e=>{let t=G.current[e];if(t){let e=t.getBoundingClientRect(),n=window.pageYOffset+e.top-14;window.scrollTo({top:Math.max(0,n),behavior:`smooth`})}},Be=async e=>{if(!_)return!1;let t=W.current;j(null),I(e.key),H(t=>({...t,[e.key]:!0}));try{let n=await le(_.service,_.key,e.key);if(t!==W.current)return!1;let r=!!n.verified,i=n.stdout||JSON.stringify(n.verification||n.json||n,null,2);return B(t=>({...t,[e.key]:{status:r?`Verified`:`Not verified`,body:i}})),V(t=>{let n={...t,[e.key]:r};return _.steps?.every(t=>t.key===e.key?r:n[t.key])&&h(_.service,_.key,!0),n}),r}catch(n){return t===W.current&&B(t=>({...t,[e.key]:{status:`Failed`,body:n.message||`Execution error`}})),!1}finally{t===W.current&&I(null)}},Ve=async()=>{if(!_||L)return;R(!0),U.current=!1;let e=W.current,t=_.steps||[];for(let n=0;nsetTimeout(e,500)),U.current||e!==W.current)break;if(!await Be(r)){e===W.current&&j(`Run all stopped because this step failed or could not be verified. Review its result and retry.`);break}await new Promise(e=>setTimeout(e,1400))}}e===W.current&&R(!1)},He=async()=>{if(!_)return;let e=++W.current;j(null),U.current=!0,R(!1),k(!0);try{if(await oe(_.service,_.key),e!==W.current)return;V({}),B({}),h(_.service,_.key,!1)}catch(t){e===W.current&&j(t.message||`Failed to reset lab`)}finally{e===W.current&&k(!1)}},[Ue,We]=(0,A.useState)(!1),[Ge,Ke]=(0,A.useState)(!1),[qe,X]=(0,A.useState)(null),Je=async()=>{Ke(!0),X(null);try{let e=await he();f({}),X({type:`success`,text:`Successfully reset all ${e.reset_lab_count||D} labs in the system.`}),We(!1),await q()}catch(e){X({type:`error`,text:e.message||`Failed to reset all labs`})}finally{Ke(!1)}},Ye=[...e].sort((e,t)=>{let n=w(e.service,e.key),r=w(t.service,t.key);return(n?n.id:999)-(r?r.id:999)}).filter(e=>{let t=w(e.service,e.key),n=N===`all`||t?.levelId===N,r=!M||e.title.toLowerCase().includes(M.toLowerCase())||e.service.toLowerCase().includes(M.toLowerCase())||e.description.toLowerCase().includes(M.toLowerCase())||(e.exams||[]).some(e=>je(e).toLowerCase().includes(M.toLowerCase()))||String(t?.id).includes(M);return n&&r}),Xe=n?.exams?.find(e=>e.code===o),Ze=o!==Y&&(!!Xe||!n&&!i||!!i),Qe=Object.fromEntries((n?.concepts||[]).map(e=>[e.key,e])),$e=(t,n)=>{let r=e.find(e=>e.service===t&&e.key===n);r&&J(r)},et=_?w(_.service,_.key):void 0,tt=_?S(_.service,_.key):void 0,Z=et?et.id:-1,Q=Z>1?x[Z-2]:void 0,$=Z>0&&Z`/labs?service=${encodeURIComponent(e.service)}&lab=${encodeURIComponent(e.key)}#${se}`,rt=t=>n=>{let r=e.find(e=>e.service===t.service&&e.key===t.key);r&&(n.preventDefault(),J(r))},it=_?.steps?.filter(e=>ke[e.key]||e.status?.verified).length||0,at=_?.steps?.length||1,ot=Math.round(it/at*100),st=it===at&&at>0||!!d[`${_?.service}:${_?.key}`];return _?(0,z.jsx)(`div`,{id:C,style:{scrollMarginTop:`16px`},children:(0,z.jsxs)(g,{size:`l`,children:[xe&&(0,z.jsx)(y,{type:`error`,children:xe}),(0,z.jsx)(p,{header:(0,z.jsx)(l,{variant:`h1`,description:_.description,actions:(0,z.jsxs)(g,{direction:`horizontal`,size:`xs`,children:[(0,z.jsx)(c,{onClick:()=>Re(),children:`← Back to Labs Directory`}),(0,z.jsx)(c,{onClick:He,loading:O,disabled:L||F!==null,children:`Reset lab`}),(0,z.jsx)(c,{variant:`primary`,iconName:`caret-right-filled`,onClick:Ve,loading:L,disabled:st||O||F!==null,children:st?`✓ Lab Fully Completed`:L?`Running steps...`:`▶ Run all remaining steps`})]}),children:(0,z.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`8px`,flexWrap:`wrap`},children:[et&&(0,z.jsxs)(E,{color:`grey`,children:[`Lesson #`,et.id,` of `,D]}),tt&&(0,z.jsx)(E,{color:tt.color,children:tt.badge}),(0,z.jsx)(E,{color:`blue`,children:_.service.toUpperCase()}),Ie(_.exams).map(e=>(0,z.jsx)(`span`,{title:`${e.exam} task ${e.task}${e.primary?` (main focus)`:``}`,children:(0,z.jsx)(E,{color:e.primary?`green`:`grey`,children:je(e)})},`${e.exam}-${e.task}`)),(0,z.jsx)(`span`,{style:{fontSize:`18px`,fontWeight:600},children:_.title}),st&&(0,z.jsx)(T,{type:`success`,children:`Completed`})]})}),children:(0,z.jsx)(Ee,{value:ot,label:`Lab Step Completion`,description:`${it} of ${at} steps complete (${ot}%)`,status:ot===100?`success`:`in-progress`})}),(0,z.jsxs)(g,{size:`m`,children:[(0,z.jsx)(`style`,{children:` +import{At as e,Ci as t,Fr as n,Ht as r,Mr as i,Ot as a,Pr as o,Ur as s,a as c,c as l,ci as u,ir as d,kt as f,l as p,o as m,pn as h,s as g,sr as _,xi as v,xr as ee,yi as y}from"./internal.js";import{$ as te,A as b,Ai as ne,J as x,K as S,M as re,Mi as ie,Q as ae,X as oe,Xa as se,Y as ce,Z as C,a as le,ao as ue,i as de,j as w,ji as fe,ki as pe,l as T,n as me,o as E,q as D,qa as he,r as ge,s as _e,u as ve}from"./app.js";import{t as O}from"./CodeSnippet.js";import{t as ye}from"./tabs.js";import{t as be}from"./column-layout.js";import{t as k}from"./link.js";import{t as xe}from"./radio-group.js";import{t as Se}from"./segmented-control.js";var A=t(v());function j(e){if(e?.progressBar)return{[d.progressBarBackgroundColor]:e.progressBar.backgroundColor,[d.progressBarBorderRadius]:e.progressBar.borderRadius,[d.progressBarHeight]:e.progressBar.height}}function M(e){if(e?.progressValue)return{[d.progressValueBackgroundColor]:e.progressValue.backgroundColor}}function Ce(e){if(e?.progressPercentage)return{color:e.progressPercentage.color,fontSize:e.progressPercentage.fontSize,fontWeight:e.progressPercentage.fontWeight}}var N={progress:`awsui_progress_11huc_15p30_13`,"result-state":`awsui_result-state_11huc_15p30_27`,"awsui-motion-fade-in":`awsui_awsui-motion-fade-in_11huc_15p30_1`,root:`awsui_root_11huc_15p30_49`,"with-result-button":`awsui_with-result-button_11huc_15p30_81`,"result-text":`awsui_result-text_11huc_15p30_85`,"result-container-error":`awsui_result-container-error_11huc_15p30_89`,"result-container-success":`awsui_result-container-success_11huc_15p30_89`,"result-button":`awsui_result-button_11huc_15p30_93`,"word-wrap":`awsui_word-wrap_11huc_15p30_99`,"label-flash":`awsui_label-flash_11huc_15p30_104`,"label-key-value":`awsui_label-key-value_11huc_15p30_108`,flash:`awsui_flash_11huc_15p30_116`,"progress-container":`awsui_progress-container_11huc_15p30_120`,"percentage-container":`awsui_percentage-container_11huc_15p30_127`,percentage:`awsui_percentage_11huc_15p30_127`,complete:`awsui_complete_11huc_15p30_173`,"progress-flash":`awsui_progress-flash_11huc_15p30_186`,"additional-info":`awsui_additional-info_11huc_15p30_199`},we=100,P=(e,t,n)=>Math.max(Math.min(e,n),t),F=({value:e,isInFlash:t,ariaLabel:n,ariaLabelledby:r,ariaDescribedby:a,style:o})=>{let s=P(Math.round(e),0,we),c=j(o),l=M(o),u=Ce(o);return A.createElement(`div`,{className:N[`progress-container`]},A.createElement(`progress`,{className:i(N.progress,s>=we&&N.complete,t&&N[`progress-flash`]),max:we,value:s,"aria-label":n,"aria-labelledby":n?void 0:r,"aria-describedby":a,style:{...c||{},...l||{}}}),A.createElement(`span`,{"aria-hidden":`true`,className:N[`percentage-container`]},A.createElement(f,{className:N.percentage,variant:`small`,color:t?`inherit`:void 0,nativeAttributes:u?{style:u}:void 0},`${s}%`)))},I=({color:e,children:t,className:n,id:r})=>A.createElement(f,{className:i(N[`word-wrap`],n),variant:`small`,display:`block`,color:e,id:r},t),L=({onClick:e,children:t})=>A.createElement(`div`,{className:N[`result-button`]},A.createElement(r,{formAction:`none`,onClick:e},t)),Te=({isInFlash:t,resultText:n,resultButtonText:r,status:a,onClick:o})=>{let s=!!r;return t?A.createElement(`div`,{className:N[`result-container-${a}`]},A.createElement(`span`,{className:N[`result-text`]},n)):A.createElement(`div`,{className:N[`result-container-${a}`]},A.createElement(`span`,{className:i(s&&N[`with-result-button`])},A.createElement(e,{type:a===`success`?`success`:`error`},A.createElement(`span`,{className:N[`result-text`]},n))),s&&A.createElement(L,{onClick:o},r))},Ee=5e3;function De({value:e=0,status:t=`in-progress`,variant:r=`standalone`,resultButtonText:o,label:c,ariaLabel:l,ariaLabelledby:d,ariaDescribedby:f,description:p,additionalInfo:m,resultText:g,onResultButtonClick:v,style:y,...te}){let{__internalRootRef:b}=n(`ProgressBar`,{props:{variant:r}}),ne=ee(te),x=`${s(`awsui-progress-bar-`)}-label`,S=r===`flash`,ie=t===`in-progress`,ae=s(`progressbar-description-`),oe=s(`progressbar-additional-info-`),[se,ce]=(0,A.useState)(``),C=re(e=>{ce(`${e}%`)},Ee,[]);return(0,A.useEffect)(()=>(C(e),()=>C.cancel()),[C,e]),S&&o&&u(`ProgressBar`,'The `resultButtonText` is ignored if you set `variant="flash"`, and the result button is not displayed. Use the `buttonText` property and the `onButtonClick` event listener of the flashbar item in which the progress bar component is embedded.'),A.createElement(`div`,{...ne,className:i(ne.className,N.root,r&&N[r]),ref:b},A.createElement(`div`,{className:S?N[`flash-container`]:void 0},A.createElement(`div`,{className:i(N[`word-wrap`],N[`label-${r}`]),id:x},c),p&&A.createElement(I,{color:S?`inherit`:void 0,id:ae},p),A.createElement(`div`,null,ie?A.createElement(A.Fragment,null,A.createElement(F,{value:e,ariaLabel:l,ariaLabelledby:a(x,d),ariaDescribedby:a(p?ae:void 0,m?oe:void 0,f),isInFlash:S,style:y}),A.createElement(h,{hidden:!0,tagName:`span`,delay:0},c,c?`: `:null,se)):A.createElement(h,{hidden:!1,tagName:`span`,delay:0},A.createElement(Te,{resultText:g,isInFlash:S,resultButtonText:o,status:t,onClick:()=>{_(v)}})))),m&&A.createElement(I,{className:N[`additional-info`],color:S?`inherit`:void 0,id:oe},m))}o(De,`ProgressBar`);var R=y(),Oe=`floci_quiz_scores`,z=`floci:quiz-scores-changed`;function B(){try{let e=localStorage.getItem(Oe);return e?JSON.parse(e):{}}catch{return{}}}function ke(e,t,n){let r=B(),i=r[e];if(i&&i.total===n&&i.correct>=t)return;let a={...r,[e]:{correct:t,total:n}};try{localStorage.setItem(Oe,JSON.stringify(a))}catch{}window.dispatchEvent(new CustomEvent(z,{detail:a}))}function V(){let[e,t]=(0,A.useState)(B);return(0,A.useEffect)(()=>{let e=e=>t(e.detail);return window.addEventListener(z,e),()=>window.removeEventListener(z,e)},[]),e}var Ae=e=>!!(e&&e.total>0&&e.correct/e.total>=.7),H=(e,t)=>e.length===t.length&&t.every(t=>e.includes(t)),U=[``,`ONE`,`TWO`,`THREE`,`FOUR`],W=({quizId:e,questions:t,best:n})=>{let[r,i]=(0,A.useState)(()=>t.map(()=>[])),[a,o]=(0,A.useState)(!1),s=t.filter((e,t)=>H(r[t],e.answers)).length,l=t.every((e,t)=>r[t].length===e.answers.length),u=(e,t,n,r)=>{i(i=>i.map((i,a)=>a===e?n?r?[...i.filter(e=>e!==t),t]:i.filter(e=>e!==t):[t]:i))},d=()=>{o(!0),ke(e,s,t.length)},f=()=>{i(t.map(()=>[])),o(!1)},p=n?` · best ${n.correct}/${n.total}`:``;return(0,R.jsx)(w,{headerText:`Practice quiz: ${t.length} questions${p}`,variant:`footer`,children:(0,R.jsx)(`div`,{"data-testid":`quiz-${e}`,children:(0,R.jsxs)(g,{size:`m`,children:[t.map((e,t)=>{let n=e.answers.length>1,i=r[t],o=H(i,e.answers),s=e.options.map((e,t)=>({value:String(t),label:e}));return(0,R.jsx)(`div`,{"data-testid":`quiz-question-${t+1}`,children:(0,R.jsxs)(g,{size:`xxs`,children:[(0,R.jsxs)(m,{variant:`strong`,children:[t+1,`. `,e.question]}),n?(0,R.jsxs)(g,{size:`xxs`,children:[e.options.map((n,r)=>(0,R.jsx)(me,{checked:i.includes(r),disabled:a||!i.includes(r)&&i.length>=e.answers.length,onChange:({detail:e})=>u(t,r,!0,e.checked),children:n},n)),!e.question.includes(`(Choose`)&&(0,R.jsxs)(m,{variant:`small`,children:[`Choose `,U[e.answers.length]||e.answers.length,`.`]})]}):(0,R.jsx)(xe,{value:i.length?String(i[0]):null,items:s.map(e=>({...e,disabled:a})),onChange:({detail:e})=>u(t,Number(e.value),!1,!0)}),a&&(0,R.jsxs)(g,{size:`xxs`,children:[(0,R.jsx)(T,{type:o?`success`:`error`,children:o?`Correct`:`Answer: ${e.answers.map(t=>e.options[t]).join(`; `)}`}),(0,R.jsx)(m,{variant:`small`,color:`text-body-secondary`,children:e.explanation})]})]})},e.question)}),(0,R.jsx)(g,{direction:`horizontal`,size:`xs`,alignItems:`center`,children:a?(0,R.jsxs)(R.Fragment,{children:[(0,R.jsxs)(T,{type:Ae({correct:s,total:t.length})?`success`:`warning`,children:[s,` of `,t.length,` correct`]}),(0,R.jsx)(c,{onClick:f,children:`Try again`})]}):(0,R.jsx)(c,{variant:`primary`,disabled:!l,onClick:d,children:`Check answers`})})]})})})};function je(e){return`${e.exam.split(`-`)[0]} ${e.task}`}var G=(e,t)=>`${e}:${t}`;function K(e){return[...new Set(e.tasks.flatMap(e=>e.labs.map(e=>G(e.service,e.key))))]}function Me(e,t){let n=0,r=0,i=0;for(let a of e.domains){let e=K(a);e.length>0&&(n+=a.weight*(e.filter(e=>t[e]).length/e.length)),i+=a.tasks.length,r+=a.tasks.filter(e=>e.labs.length>0).length}return{weighted:Math.round(n),tasksWithLabs:r,taskCount:i}}var Ne=({service:e,labKey:t,title:n,primary:r,completed:i,onOpenLab:a})=>{let o=C(e,t),s=!!i[G(e,t)];return(0,R.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`6px`,flexWrap:`wrap`},children:[s?(0,R.jsx)(T,{type:`success`}):(0,R.jsx)(T,{type:`pending`}),o&&(0,R.jsxs)(E,{color:`grey`,children:[`#`,o.id]}),(0,R.jsx)(E,{color:`blue`,children:e.toUpperCase()}),(0,R.jsx)(c,{variant:`inline-link`,onClick:()=>a(e,t),children:r?(0,R.jsx)(`strong`,{children:n}):n})]})},q=({card:e,completed:t,onOpenLab:n})=>(0,R.jsx)(w,{headerText:`Concept: ${e.title}`,variant:`footer`,children:(0,R.jsxs)(g,{size:`xs`,children:[(0,R.jsx)(`ul`,{style:{margin:0,paddingLeft:`20px`},children:e.points.map(e=>(0,R.jsx)(`li`,{children:e},e))}),e.related_labs.length>0&&(0,R.jsxs)(m,{variant:`small`,children:[`Related labs:`,` `,e.related_labs.map((e,r)=>{let i=C(e.service,e.key);return(0,R.jsxs)(A.Fragment,{children:[r>0&&`, `,(0,R.jsx)(c,{variant:`inline-link`,onClick:()=>n(e.service,e.key),children:i?`#${i.id} ${i.title}`:`${e.service}/${e.key}`}),t[G(e.service,e.key)]?` ✓`:``]},G(e.service,e.key))})]}),e.planned&&(0,R.jsxs)(m,{variant:`small`,color:`text-body-secondary`,children:[`Planned lab: `,e.planned]})]})}),Pe=({exam:e,concepts:t,completed:n,onOpenLab:r})=>{let i=Me(e,n),a=V(),o=e.domains.flatMap(e=>e.tasks).filter(e=>(e.quiz?.length??0)>0),s=o.filter(t=>Ae(a[`${e.code}:${t.id}`])).length;return(0,R.jsxs)(g,{size:`m`,children:[(0,R.jsxs)(be,{columns:2,variant:`text-grid`,children:[(0,R.jsxs)(g,{size:`xxs`,children:[(0,R.jsxs)(m,{variant:`h2`,children:[e.code,` · `,e.title]}),(0,R.jsxs)(m,{variant:`small`,color:`text-body-secondary`,children:[`Domains and task statements from the`,` `,(0,R.jsx)(k,{href:e.guide_url,external:!0,children:`official exam guide`}),` `,`(checked `,e.checked,`). Labs show the tasks they practice; concept cards cover objectives that can't be practiced in a local emulator.`]})]}),(0,R.jsxs)(g,{size:`xs`,children:[(0,R.jsx)(De,{value:i.weighted,label:`Exam-weighted lab progress`,description:`Completed labs, weighted by each domain's share of the exam. Hands-on labs cover ${i.tasksWithLabs} of ${i.taskCount} task statements; the rest have concept cards.`,status:i.weighted===100?`success`:`in-progress`}),o.length>0&&(0,R.jsx)(`div`,{"data-testid":`quiz-progress-${e.code}`,children:(0,R.jsxs)(m,{variant:`small`,color:`text-body-secondary`,children:[`Practice quizzes passed: `,s,` of `,o.length,` task statements (70% or better).`]})})]})]}),e.domains.map((i,o)=>{let s=K(i),c=s.filter(e=>n[e]).length;return(0,R.jsx)(w,{variant:`container`,defaultExpanded:o===0,headerText:`Domain ${i.id}: ${i.title}`,headerDescription:`${i.weight}% of scored content · ${s.length>0?`${c} of ${s.length} labs complete`:`no hands-on labs yet`}`,headerActions:(0,R.jsxs)(E,{color:s.length>0?`blue`:`grey`,children:[i.weight,`%`]}),children:(0,R.jsx)(g,{size:`l`,children:i.tasks.map(i=>(0,R.jsx)(`div`,{"data-testid":`exam-task-${e.code}-${i.id}`,children:(0,R.jsxs)(g,{size:`xs`,children:[(0,R.jsxs)(m,{variant:`h4`,children:[i.id,` `,i.title]}),i.labs.length>0?(0,R.jsx)(g,{size:`xxs`,children:i.labs.map(e=>(0,R.jsx)(Ne,{service:e.service,labKey:e.key,title:e.title,primary:e.primary,completed:n,onOpenLab:r},G(e.service,e.key)))}):(0,R.jsx)(T,{type:`warning`,children:`No hands-on lab yet`}),i.concepts.map(e=>t[e]).filter(Boolean).map(e=>(0,R.jsx)(q,{card:e,completed:n,onOpenLab:r},e.key)),i.quiz&&i.quiz.length>0&&(0,R.jsx)(W,{quizId:`${e.code}:${i.id}`,questions:i.quiz,best:a[`${e.code}:${i.id}`]})]})},i.id))})},i.id)}),(0,R.jsx)(m,{variant:`small`,color:`text-body-secondary`,children:`Bold labs practice this task statement as their main focus; the rest touch it along the way.`})]})},Fe=`floci_labs_study_track`,J=`levels`;function Ie(){let e=new URLSearchParams(window.location.search).get(`track`);if(e)return e;try{return localStorage.getItem(Fe)||J}catch{return J}}function Le(e=[]){return[...e].sort((e,t)=>Number(t.primary)-Number(e.primary)||e.exam.localeCompare(t.exam)||e.task.localeCompare(t.task,void 0,{numeric:!0}))}var Y=()=>{let[e,t]=(0,A.useState)([]),[n,r]=(0,A.useState)(null),[i,a]=(0,A.useState)(null),[o,s]=(0,A.useState)(Ie),u=e=>{s(e);try{localStorage.setItem(Fe,e)}catch{}let t=new URL(window.location.href);e===J?t.searchParams.delete(`track`):t.searchParams.set(`track`,e),window.history.replaceState(null,``,t.pathname+t.search)},[d,f]=(0,A.useState)(de);(0,A.useEffect)(()=>{le(d)},[d]);let h=(e,t,n)=>{f(r=>({...r,[`${e}:${t}`]:n}))},[_,v]=(0,A.useState)(null),[ee,y]=(0,A.useState)(!0),[re,me]=(0,A.useState)(null),[be,k]=(0,A.useState)(!1),[xe,j]=(0,A.useState)(null),[M,Ce]=(0,A.useState)(``),[N,we]=(0,A.useState)(`all`),[P,F]=(0,A.useState)(null),[I,L]=(0,A.useState)(!1),[Te,Ee]=(0,A.useState)(()=>{try{let e=localStorage.getItem(`floci_labs_active_sdk`);if(e===`cli`||e===`boto3`||e===`terraform`)return e}catch{}return`cli`}),Oe=e=>{Ee(e);try{localStorage.setItem(`floci_labs_active_sdk`,e)}catch{}},[z,B]=(0,A.useState)({}),[ke,V]=(0,A.useState)({}),[Ae,H]=(0,A.useState)({}),U=(0,A.useRef)(!1),W=(0,A.useRef)(0),G=(0,A.useRef)({}),[K,Me]=(0,A.useState)(null),Ne=(0,A.useRef)([]),q=async()=>{y(!0),me(null);try{let e=((await fe()).services||[]).flatMap(e=>e.labs);Ne.current=e,t(e),a(null),pe().then(r).catch(e=>a(e?.message||`Could not load the exam study tracks.`));let n=new URLSearchParams(window.location.search),i=n.get(`lab`),o=n.get(`service`);if(i){let t=e.find(e=>e.key===i&&(!o||e.service===o));t&&Y(t,!1)}ie().then(e=>{f(t=>{let n={...t};return(e.labs||[]).forEach(e=>{let t=e.key||e.lab;t&&(n[`${e.service}:${t}`]=!!e.complete)}),n})})}catch(e){console.error(e),me(e?.message||`Could not load the lab catalog.`)}finally{y(!1)}};(0,A.useEffect)(()=>{q()},[]),(0,A.useEffect)(()=>{let e=()=>{let e=new URLSearchParams(window.location.search),t=e.get(`lab`),n=e.get(`service`),r=Ne.current;if(t&&r.length>0){let e=r.find(e=>e.key===t&&(!n||e.service===n));e&&Y(e,!1)}else!t&&_&&ze(!1)};return window.addEventListener(`popstate`,e),window.addEventListener(`floci_navigate`,e),()=>{window.removeEventListener(`popstate`,e),window.removeEventListener(`floci_navigate`,e)}},[_]);let Y=async(e,t=!0)=>{U.current=!0;let n=++W.current;if(L(!1),F(null),j(null),v(e),B({}),V({}),H({}),k(!0),t){let t=new URL(window.location.href);t.searchParams.set(`lab`,e.key),t.searchParams.set(`service`,e.service),t.hash=ce,window.history.pushState(null,``,t.pathname+t.search+t.hash),window.dispatchEvent(new Event(`floci_url_changed`))}try{let t=await ne(e.service,e.key);if(n!==W.current)return;if(t.error)throw Error(t.error);let r={},i={};if(Object.entries(t.steps||{}).forEach(([e,t])=>{r[e]=!!t.verified,t.verification&&(i[e]={status:t.verified?`Verified`:`Unverified`,body:t.verification.message||JSON.stringify(t.verification,null,2)})}),V(r),B(i),window.location.hash===`#next-step`){let t=e.steps||[],n=t.findIndex(e=>!(r[e.key]||e.status?.verified));Me(n>0?te(t[n].key):oe)}h(e.service,e.key,!!t.complete)}catch(e){n===W.current&&j(e.message||`Failed to load lab status`)}finally{n===W.current&&k(!1)}},Re=_?`${_.service}:${_.key}`:``;(0,A.useLayoutEffect)(()=>{if(!Re)return;let e=(window.location.hash.startsWith(`#step-`)?document.getElementById(window.location.hash.slice(1)):null)||document.getElementById(`lab-top`);e?e.scrollIntoView({block:`start`}):window.scrollTo({top:0})},[Re]),(0,A.useLayoutEffect)(()=>{if(!K||(Me(null),window.location.hash!==`#next-step`))return;let e=document.getElementById(K);e&&e.scrollIntoView({block:`start`}),K!==`lab-top`&&window.history.replaceState(window.history.state,``,`${window.location.pathname}${window.location.search}#${K}`)},[K]);let ze=(e=!0)=>{if(++W.current,U.current=!0,L(!1),v(null),B({}),V({}),H({}),e){let e=new URL(window.location.href);e.searchParams.delete(`lab`),e.searchParams.delete(`service`),e.hash=``,window.history.pushState(null,``,e.pathname+e.search),window.dispatchEvent(new Event(`floci_url_changed`))}},Be=e=>{let t=G.current[e];if(t){let e=t.getBoundingClientRect(),n=window.pageYOffset+e.top-14;window.scrollTo({top:Math.max(0,n),behavior:`smooth`})}},Ve=async e=>{if(!_)return!1;let t=W.current;j(null),F(e.key),H(t=>({...t,[e.key]:!0}));try{let n=await ue(_.service,_.key,e.key);if(t!==W.current)return!1;let r=!!n.verified,i=n.stdout||JSON.stringify(n.verification||n.json||n,null,2);return B(t=>({...t,[e.key]:{status:r?`Verified`:`Not verified`,body:i}})),V(t=>{let n={...t,[e.key]:r};return _.steps?.every(t=>t.key===e.key?r:n[t.key])&&h(_.service,_.key,!0),n}),r}catch(n){return t===W.current&&B(t=>({...t,[e.key]:{status:`Failed`,body:n.message||`Execution error`}})),!1}finally{t===W.current&&F(null)}},He=async()=>{if(!_||I)return;L(!0),U.current=!1;let e=W.current,t=_.steps||[];for(let n=0;nsetTimeout(e,500)),U.current||e!==W.current)break;if(!await Ve(r)){e===W.current&&j(`Run all stopped because this step failed or could not be verified. Review its result and retry.`);break}await new Promise(e=>setTimeout(e,1400))}}e===W.current&&L(!1)},Ue=async()=>{if(!_)return;let e=++W.current;j(null),U.current=!0,L(!1),k(!0);try{if(await se(_.service,_.key),e!==W.current)return;V({}),B({}),h(_.service,_.key,!1)}catch(t){e===W.current&&j(t.message||`Failed to reset lab`)}finally{e===W.current&&k(!1)}},[We,Ge]=(0,A.useState)(!1),[Ke,qe]=(0,A.useState)(!1),[Je,Ye]=(0,A.useState)(null),Xe=async()=>{qe(!0),Ye(null);try{let e=await he();f({}),Ye({type:`success`,text:`Successfully reset all ${e.reset_lab_count||D} labs in the system.`}),Ge(!1),await q()}catch(e){Ye({type:`error`,text:e.message||`Failed to reset all labs`})}finally{qe(!1)}},Ze=[...e].sort((e,t)=>{let n=C(e.service,e.key),r=C(t.service,t.key);return(n?n.id:999)-(r?r.id:999)}).filter(e=>{let t=C(e.service,e.key),n=N===`all`||t?.levelId===N,r=!M||e.title.toLowerCase().includes(M.toLowerCase())||e.service.toLowerCase().includes(M.toLowerCase())||e.description.toLowerCase().includes(M.toLowerCase())||(e.exams||[]).some(e=>je(e).toLowerCase().includes(M.toLowerCase()))||String(t?.id).includes(M);return n&&r}),Qe=n?.exams?.find(e=>e.code===o),$e=o!==J&&(!!Qe||!n&&!i||!!i),et=Object.fromEntries((n?.concepts||[]).map(e=>[e.key,e])),tt=(t,n)=>{let r=e.find(e=>e.service===t&&e.key===n);r&&Y(r)},X=_?C(_.service,_.key):void 0,nt=_?ae(_.service,_.key):void 0,Z=X?X.id:-1,Q=Z>1?S[Z-2]:void 0,$=Z>0&&Z`/labs?service=${encodeURIComponent(e.service)}&lab=${encodeURIComponent(e.key)}#${ce}`,it=t=>n=>{let r=e.find(e=>e.service===t.service&&e.key===t.key);r&&(n.preventDefault(),Y(r))},at=_?.steps?.filter(e=>ke[e.key]||e.status?.verified).length||0,ot=_?.steps?.length||1,st=Math.round(at/ot*100),ct=at===ot&&ot>0||!!d[`${_?.service}:${_?.key}`];return _?(0,R.jsx)(`div`,{id:oe,style:{scrollMarginTop:`16px`},children:(0,R.jsxs)(g,{size:`l`,children:[xe&&(0,R.jsx)(b,{type:`error`,children:xe}),(0,R.jsx)(p,{header:(0,R.jsx)(l,{variant:`h1`,description:_.description,actions:(0,R.jsxs)(g,{direction:`horizontal`,size:`xs`,children:[(0,R.jsx)(c,{onClick:()=>ze(),children:`← Back to Labs Directory`}),(0,R.jsx)(c,{onClick:Ue,loading:be,disabled:I||P!==null,children:`Reset lab`}),(0,R.jsx)(c,{variant:`primary`,iconName:`caret-right-filled`,onClick:He,loading:I,disabled:ct||be||P!==null,children:ct?`✓ Lab Fully Completed`:I?`Running steps...`:`▶ Run all remaining steps`})]}),children:(0,R.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`8px`,flexWrap:`wrap`},children:[X&&(0,R.jsxs)(E,{color:`grey`,children:[`Lesson #`,X.id,` of `,D]}),nt&&(0,R.jsx)(E,{color:nt.color,children:nt.badge}),(0,R.jsx)(E,{color:`blue`,children:_.service.toUpperCase()}),Le(_.exams).map(e=>(0,R.jsx)(`span`,{title:`${e.exam} task ${e.task}${e.primary?` (main focus)`:``}`,children:(0,R.jsx)(E,{color:e.primary?`green`:`grey`,children:je(e)})},`${e.exam}-${e.task}`)),(0,R.jsx)(`span`,{style:{fontSize:`18px`,fontWeight:600},children:_.title}),ct&&(0,R.jsx)(T,{type:`success`,children:`Completed`})]})}),children:(0,R.jsx)(De,{value:st,label:`Lab Step Completion`,description:`${at} of ${ot} steps complete (${st}%)`,status:st===100?`success`:`in-progress`})}),(0,R.jsxs)(g,{size:`m`,children:[(0,R.jsx)(`style`,{children:` .lab-step-completed > div { border-color: #1d8102 !important; box-shadow: 0 0 0 1px #1d8102 !important; } - `}),_.steps?.map((e,t)=>{let n=!!(ke[e.key]||e.status?.verified),r=Oe[e.key];return(0,z.jsx)(`div`,{id:ne(e.key),ref:t=>{G.current[e.key]=t},className:n?`lab-step-completed`:`lab-step-pending`,style:{scrollMarginTop:`16px`,borderRadius:`16px`,transition:`all 0.2s ease`},children:(0,z.jsx)(p,{header:(0,z.jsxs)(l,{variant:`h3`,actions:(0,z.jsxs)(g,{direction:`horizontal`,size:`xs`,children:[n&&(0,z.jsx)(T,{type:`success`,children:`Verified`}),(0,z.jsx)(c,{variant:n?`normal`:`primary`,iconName:n?`status-positive`:`caret-right-filled`,onClick:()=>Be(e),loading:F===e.key,disabled:n||L||O||F!==null,children:n?`✓ Step Complete`:`Run step`})]}),children:[`Step `,t+1,`: `,e.title]}),children:(0,z.jsxs)(g,{size:`m`,children:[(0,z.jsx)(be,{activeTabId:we,onChange:({detail:e})=>De(e.activeTabId),tabs:[{label:`AWS CLI`,id:`cli`,content:(0,z.jsx)(ye,{language:`cli`,code:e.command})},{label:`Python (boto3)`,id:`boto3`,content:(0,z.jsx)(ye,{language:`boto3`,code:e.snippets?.boto3||e.command})},{label:`Terraform (HCL)`,id:`terraform`,content:(0,z.jsx)(ye,{language:`terraform`,code:e.snippets?.terraform||e.command})}]}),(0,z.jsx)(de,{headerText:`Explain command & concept`,expanded:!!Ae[e.key],onChange:({detail:t})=>H(n=>({...n,[e.key]:t.expanded})),children:(0,z.jsx)(`p`,{style:{lineHeight:`1.6`,margin:0},children:e.explanation})}),r&&(0,z.jsx)(p,{header:(0,z.jsx)(l,{variant:`h3`,children:(0,z.jsxs)(T,{type:r.status===`Failed`?`error`:r.status===`Not verified`?`warning`:`success`,children:[`Execution Result: `,r.status]})}),children:(0,z.jsx)(ye,{language:`json`,code:r.body})})]})})},e.key)})]}),(0,z.jsx)(p,{children:(0,z.jsxs)(`div`,{style:{display:`flex`,justifyContent:`space-between`,alignItems:`center`,flexWrap:`wrap`,gap:`12px`},children:[Q?(0,z.jsxs)(c,{href:nt(Q),onFollow:rt(Q),children:[`← Previous: #`,Q.id,`. [`,Q.service.toUpperCase(),`] `,Q.title]}):(0,z.jsx)(`div`,{}),$&&(0,z.jsxs)(c,{variant:`primary`,href:nt($),onFollow:rt($),children:[`Next: #`,$.id,`. [`,$.service.toUpperCase(),`] `,$.title,` →`]})]})})]})}):(0,z.jsxs)(g,{size:`l`,children:[qe&&(0,z.jsx)(y,{type:qe.type,dismissible:!0,onDismiss:()=>X(null),children:qe.text}),(0,z.jsx)(p,{header:(0,z.jsxs)(l,{variant:`h1`,description:`Learn AWS hands-on, from foundations to private networking, developer workflows, governance, and cost, across ${D} lessons in ${b.length} progressive levels.`,actions:(0,z.jsxs)(g,{direction:`horizontal`,size:`xs`,children:[(0,z.jsx)(c,{variant:`normal`,iconName:`remove`,onClick:()=>We(!0),loading:Ge,children:`Reset All Labs`}),(0,z.jsx)(c,{iconName:`refresh`,onClick:q,loading:te,children:`Refresh catalog`})]}),children:[`AWS Workflow Labs Curriculum (`,D,` Progressive Lessons)`]}),children:(0,z.jsxs)(g,{size:`m`,children:[(0,z.jsx)(`div`,{style:{overflowX:`auto`,paddingBottom:`4px`},children:(0,z.jsx)(Se,{label:`Study track`,selectedId:Ze?o:Y,onChange:({detail:e})=>u(e.selectedId),options:[{id:Y,text:`Levels`},...(n?.exams||[]).map(e=>({id:e.code,text:`${e.code} ${e.short_title}`}))]})}),Ze?Xe?(0,z.jsx)(Ne,{exam:Xe,concepts:Qe,completed:d,onOpenLab:$e}):i?(0,z.jsxs)(y,{type:`error`,action:(0,z.jsx)(c,{onClick:q,children:`Retry`}),children:[`Could not load the exam study tracks: `,i]}):(0,z.jsx)(T,{type:`loading`,children:`Loading exam study track`}):(0,z.jsxs)(z.Fragment,{children:[(0,z.jsx)(`div`,{style:{overflowX:`auto`,paddingBottom:`4px`},children:(0,z.jsx)(Se,{selectedId:N,onChange:({detail:e})=>P(e.selectedId),options:[{id:`all`,text:`All ${D} Labs`},...b.map(e=>({id:e.id,text:`${e.title} (${e.labs.length})`}))]})}),(0,z.jsx)(_e,{filteringText:M,filteringPlaceholder:`Filter labs by number (#1-#${D}), service (S3, IAM, EC2, EKS), exam task (SAA 2.1), or keyword...`,onChange:({detail:e})=>Ce(e.filteringText)}),(0,z.jsx)(ve,{cardDefinition:{header:e=>{let t=!!d[`${e.service}:${e.key}`],n=w(e.service,e.key),r=S(e.service,e.key);return(0,z.jsxs)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:`8px`},children:[(0,z.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`6px`,flexWrap:`wrap`},children:[n&&(0,z.jsxs)(E,{color:`grey`,children:[`#`,n.id]}),r&&(0,z.jsx)(E,{color:r.color,children:r.badge}),(0,z.jsx)(E,{color:`blue`,children:e.service.toUpperCase()}),Ie(e.exams).filter(e=>e.primary).map(e=>(0,z.jsx)(E,{color:`grey`,children:je(e)},`${e.exam}-${e.task}`)),t&&(0,z.jsx)(E,{color:`green`,children:`✓ COMPLETE`})]}),(0,z.jsx)(c,{variant:`inline-link`,onClick:()=>J(e),children:(0,z.jsxs)(`span`,{style:{display:`inline-flex`,alignItems:`center`,gap:`6px`,fontSize:`15px`},children:[t&&(0,z.jsx)(T,{type:`success`}),(0,z.jsx)(`strong`,{style:{color:t?`#4ec9b0`:`inherit`},children:e.title})]})})]})},sections:[{id:`description`,content:e=>{let t=!!d[`${e.service}:${e.key}`];return(0,z.jsx)(`div`,{style:{borderRadius:`6px`,padding:t?`8px 10px`:void 0,border:t?`1px solid #1d8102`:void 0,background:t?`rgba(29, 129, 2, 0.08)`:void 0},children:e.description})}},{id:`steps`,header:`Runnable Steps`,content:e=>{let t=e.step_count||e.steps?.length||1;return`${t} ${t===1?`step`:`steps`} (CLI / Boto3 / Terraform)`}},{id:`action`,content:e=>{let t=!!d[`${e.service}:${e.key}`];return(0,z.jsx)(c,{variant:t?`normal`:`primary`,iconName:t?`status-positive`:`caret-right-filled`,onClick:()=>J(e),children:t?`Review Completed Lab`:`Start Lab Lesson`})}}]},items:Ye,loading:te&&e.length===0,loadingText:`Loading lab curriculum`,cardsPerRow:[{cards:1},{minWidth:600,cards:3}],empty:ie?(0,z.jsxs)(m,{textAlign:`center`,color:`inherit`,children:[(0,z.jsx)(`b`,{children:`Lab catalog unavailable`}),(0,z.jsx)(`p`,{children:ie}),(0,z.jsx)(c,{onClick:q,children:`Retry`})]}):(0,z.jsxs)(m,{textAlign:`center`,color:`inherit`,children:[(0,z.jsx)(`b`,{children:`No matching labs found`}),(0,z.jsx)(`p`,{children:`Try searching for a different keyword or level filter.`})]})})]})]})}),(0,z.jsx)(ge,{visible:Ue,onDismiss:()=>!Ge&&We(!1),header:`Reset All ${D} Workflow Labs`,footer:(0,z.jsx)(m,{float:`right`,children:(0,z.jsxs)(g,{direction:`horizontal`,size:`xs`,children:[(0,z.jsx)(c,{variant:`link`,onClick:()=>We(!1),disabled:Ge,children:`Cancel`}),(0,z.jsx)(c,{variant:`primary`,onClick:Je,loading:Ge,children:`Reset All Labs`})]})}),children:(0,z.jsxs)(g,{size:`m`,children:[(0,z.jsxs)(y,{type:`warning`,children:[`This will tear down all lab-created AWS resources across all 20 services (S3 buckets, DynamoDB tables, Lambda functions, IAM roles/users, SQS queues, RDS instances, ECS clusters, EKS clusters, etc.) and clear completion progress for all `,D,` lessons.`]}),(0,z.jsx)(`p`,{children:`Are you sure you want to reset every lab in the system?`})]})})]})};export{Le as LabsConsole}; \ No newline at end of file + `}),_.steps?.map((e,t)=>{let n=!!(ke[e.key]||e.status?.verified),r=z[e.key];return(0,R.jsx)(`div`,{id:te(e.key),ref:t=>{G.current[e.key]=t},className:n?`lab-step-completed`:`lab-step-pending`,style:{scrollMarginTop:`16px`,borderRadius:`16px`,transition:`all 0.2s ease`},children:(0,R.jsx)(p,{header:(0,R.jsxs)(l,{variant:`h3`,actions:(0,R.jsxs)(g,{direction:`horizontal`,size:`xs`,children:[n&&(0,R.jsx)(T,{type:`success`,children:`Verified`}),(0,R.jsx)(c,{variant:n?`normal`:`primary`,iconName:n?`status-positive`:`caret-right-filled`,onClick:()=>Ve(e),loading:P===e.key,disabled:n||I||be||P!==null,children:n?`✓ Step Complete`:`Run step`})]}),children:[`Step `,t+1,`: `,e.title]}),children:(0,R.jsxs)(g,{size:`m`,children:[(0,R.jsx)(ye,{activeTabId:Te,onChange:({detail:e})=>Oe(e.activeTabId),tabs:[{label:`AWS CLI`,id:`cli`,content:(0,R.jsx)(O,{language:`cli`,code:e.command})},{label:`Python (boto3)`,id:`boto3`,content:(0,R.jsx)(O,{language:`boto3`,code:e.snippets?.boto3||e.command})},{label:`Terraform (HCL)`,id:`terraform`,content:(0,R.jsx)(O,{language:`terraform`,code:e.snippets?.terraform||e.command})}]}),(0,R.jsx)(w,{headerText:`Explain command & concept`,expanded:!!Ae[e.key],onChange:({detail:t})=>H(n=>({...n,[e.key]:t.expanded})),children:(0,R.jsx)(`p`,{style:{lineHeight:`1.6`,margin:0},children:e.explanation})}),r&&(0,R.jsx)(p,{header:(0,R.jsx)(l,{variant:`h3`,children:(0,R.jsxs)(T,{type:r.status===`Failed`?`error`:r.status===`Not verified`?`warning`:`success`,children:[`Execution Result: `,r.status]})}),children:(0,R.jsx)(O,{language:`json`,code:r.body})})]})})},e.key)})]}),(0,R.jsx)(p,{children:(0,R.jsxs)(`div`,{style:{display:`flex`,justifyContent:`space-between`,alignItems:`center`,flexWrap:`wrap`,gap:`12px`},children:[Q?(0,R.jsxs)(c,{href:rt(Q),onFollow:it(Q),children:[`← Previous: #`,Q.id,`. [`,Q.service.toUpperCase(),`] `,Q.title]}):(0,R.jsx)(`div`,{}),$&&(0,R.jsxs)(c,{variant:`primary`,href:rt($),onFollow:it($),children:[`Next: #`,$.id,`. [`,$.service.toUpperCase(),`] `,$.title,` →`]})]})})]})}):(0,R.jsxs)(g,{size:`l`,children:[Je&&(0,R.jsx)(b,{type:Je.type,dismissible:!0,onDismiss:()=>Ye(null),children:Je.text}),(0,R.jsx)(p,{header:(0,R.jsxs)(l,{variant:`h1`,description:`Learn AWS hands-on, from foundations to private networking, developer workflows, governance, and cost, across ${D} lessons in ${x.length} progressive levels.`,actions:(0,R.jsxs)(g,{direction:`horizontal`,size:`xs`,children:[(0,R.jsx)(c,{variant:`normal`,iconName:`remove`,onClick:()=>Ge(!0),loading:Ke,children:`Reset All Labs`}),(0,R.jsx)(c,{iconName:`refresh`,onClick:q,loading:ee,children:`Refresh catalog`})]}),children:[`AWS Workflow Labs Curriculum (`,D,` Progressive Lessons)`]}),children:(0,R.jsxs)(g,{size:`m`,children:[(0,R.jsx)(`div`,{style:{overflowX:`auto`,paddingBottom:`4px`},children:(0,R.jsx)(Se,{label:`Study track`,selectedId:$e?o:J,onChange:({detail:e})=>u(e.selectedId),options:[{id:J,text:`Levels`},...(n?.exams||[]).map(e=>({id:e.code,text:`${e.code} ${e.short_title}`}))]})}),$e?Qe?(0,R.jsx)(Pe,{exam:Qe,concepts:et,completed:d,onOpenLab:tt}):i?(0,R.jsxs)(b,{type:`error`,action:(0,R.jsx)(c,{onClick:q,children:`Retry`}),children:[`Could not load the exam study tracks: `,i]}):(0,R.jsx)(T,{type:`loading`,children:`Loading exam study track`}):(0,R.jsxs)(R.Fragment,{children:[(0,R.jsx)(`div`,{style:{overflowX:`auto`,paddingBottom:`4px`},children:(0,R.jsx)(Se,{selectedId:N,onChange:({detail:e})=>we(e.selectedId),options:[{id:`all`,text:`All ${D} Labs`},...x.map(e=>({id:e.id,text:`${e.title} (${e.labs.length})`}))]})}),(0,R.jsx)(_e,{filteringText:M,filteringPlaceholder:`Filter labs by number (#1-#${D}), service (S3, IAM, EC2, EKS), exam task (SAA 2.1), or keyword...`,onChange:({detail:e})=>Ce(e.filteringText)}),(0,R.jsx)(ve,{cardDefinition:{header:e=>{let t=!!d[`${e.service}:${e.key}`],n=C(e.service,e.key),r=ae(e.service,e.key);return(0,R.jsxs)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:`8px`},children:[(0,R.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:`6px`,flexWrap:`wrap`},children:[n&&(0,R.jsxs)(E,{color:`grey`,children:[`#`,n.id]}),r&&(0,R.jsx)(E,{color:r.color,children:r.badge}),(0,R.jsx)(E,{color:`blue`,children:e.service.toUpperCase()}),Le(e.exams).filter(e=>e.primary).map(e=>(0,R.jsx)(E,{color:`grey`,children:je(e)},`${e.exam}-${e.task}`)),t&&(0,R.jsx)(E,{color:`green`,children:`✓ COMPLETE`})]}),(0,R.jsx)(c,{variant:`inline-link`,onClick:()=>Y(e),children:(0,R.jsxs)(`span`,{style:{display:`inline-flex`,alignItems:`center`,gap:`6px`,fontSize:`15px`},children:[t&&(0,R.jsx)(T,{type:`success`}),(0,R.jsx)(`strong`,{style:{color:t?`#4ec9b0`:`inherit`},children:e.title})]})})]})},sections:[{id:`description`,content:e=>{let t=!!d[`${e.service}:${e.key}`];return(0,R.jsx)(`div`,{style:{borderRadius:`6px`,padding:t?`8px 10px`:void 0,border:t?`1px solid #1d8102`:void 0,background:t?`rgba(29, 129, 2, 0.08)`:void 0},children:e.description})}},{id:`steps`,header:`Runnable Steps`,content:e=>{let t=e.step_count||e.steps?.length||1;return`${t} ${t===1?`step`:`steps`} (CLI / Boto3 / Terraform)`}},{id:`action`,content:e=>{let t=!!d[`${e.service}:${e.key}`];return(0,R.jsx)(c,{variant:t?`normal`:`primary`,iconName:t?`status-positive`:`caret-right-filled`,onClick:()=>Y(e),children:t?`Review Completed Lab`:`Start Lab Lesson`})}}]},items:Ze,loading:ee&&e.length===0,loadingText:`Loading lab curriculum`,cardsPerRow:[{cards:1},{minWidth:600,cards:3}],empty:re?(0,R.jsxs)(m,{textAlign:`center`,color:`inherit`,children:[(0,R.jsx)(`b`,{children:`Lab catalog unavailable`}),(0,R.jsx)(`p`,{children:re}),(0,R.jsx)(c,{onClick:q,children:`Retry`})]}):(0,R.jsxs)(m,{textAlign:`center`,color:`inherit`,children:[(0,R.jsx)(`b`,{children:`No matching labs found`}),(0,R.jsx)(`p`,{children:`Try searching for a different keyword or level filter.`})]})})]})]})}),(0,R.jsx)(ge,{visible:We,onDismiss:()=>!Ke&&Ge(!1),header:`Reset All ${D} Workflow Labs`,footer:(0,R.jsx)(m,{float:`right`,children:(0,R.jsxs)(g,{direction:`horizontal`,size:`xs`,children:[(0,R.jsx)(c,{variant:`link`,onClick:()=>Ge(!1),disabled:Ke,children:`Cancel`}),(0,R.jsx)(c,{variant:`primary`,onClick:Xe,loading:Ke,children:`Reset All Labs`})]})}),children:(0,R.jsxs)(g,{size:`m`,children:[(0,R.jsxs)(b,{type:`warning`,children:[`This will tear down all lab-created AWS resources across all 20 services (S3 buckets, DynamoDB tables, Lambda functions, IAM roles/users, SQS queues, RDS instances, ECS clusters, EKS clusters, etc.) and clear completion progress for all `,D,` lessons.`]}),(0,R.jsx)(`p`,{children:`Are you sure you want to reset every lab in the system?`})]})})]})};export{Y as LabsConsole}; \ No newline at end of file diff --git a/frontend/src/pages/LabsConsole.tsx b/frontend/src/pages/LabsConsole.tsx index a7f55af..554e464 100644 --- a/frontend/src/pages/LabsConsole.tsx +++ b/frontend/src/pages/LabsConsole.tsx @@ -111,6 +111,9 @@ export const LabsConsole: React.FC = () => { const labRequestId = useRef(0); const stepRefs = useRef>({}); const [scrollTarget, setScrollTarget] = useState(null); + // The URL listener reads the catalog from here, so a lab link clicked just as the + // catalog arrives (before the listener re-subscribes with the new list) still opens. + const allLabsRef = useRef([]); const loadCatalog = async () => { setLoading(true); @@ -118,6 +121,7 @@ export const LabsConsole: React.FC = () => { try { const data = await fetchLabsCatalog(); const flattened = (data.services || []).flatMap((s) => s.labs); + allLabsRef.current = flattened; setAllLabsList(flattened); setExamError(null); @@ -168,8 +172,9 @@ export const LabsConsole: React.FC = () => { const urlLabKey = params.get('lab'); const urlServiceKey = params.get('service'); - if (urlLabKey && allLabsList.length > 0) { - const found = allLabsList.find((l) => l.key === urlLabKey && (!urlServiceKey || l.service === urlServiceKey)); + const labs = allLabsRef.current; + if (urlLabKey && labs.length > 0) { + const found = labs.find((l) => l.key === urlLabKey && (!urlServiceKey || l.service === urlServiceKey)); if (found) { openLab(found, false); } @@ -184,7 +189,7 @@ export const LabsConsole: React.FC = () => { window.removeEventListener('popstate', handleUrlSync); window.removeEventListener('floci_navigate', handleUrlSync); }; - }, [allLabsList, activeLab]); + }, [activeLab]); const openLab = async (lab: LabDefinition, updateUrl = true) => { cancelRunAllRef.current = true;