import { addEl, setButtons,fillValues, setValue, buildUrl, fetchJson, setVisible, enableEl, setValues, getParam, fillSelect, forEachEl, readFile } from "./helper.js"; import {load as yamlLoad} from "https://cdn.skypack.dev/js-yaml@4.1.0"; import fileDownload from "https://cdn.skypack.dev/js-file-download@0.4.12" class PipelineInfo{ constructor(id){ this.STFIELDS=['status','error','status_url']; this.reset(id); this.lastUpdate=0; } update(state){ this.lastUpdate=(new Date()).getTime(); if (state.pipeline_id !== undefined && state.pipeline_id !== this.id){ return false; } this.STFIELDS.forEach((i)=>{ let v=state[i]; if (v !== undefined)this[i]=v; }); } reset(id,opt_state){ this.id=id; this.STFIELDS.forEach((i)=>this[i]=undefined); this.downloadUrl=undefined; if (opt_state) { this.update(opt_state); } else{ if (id !== undefined) this.status='fetching'; } } valid(){ return this.id !== undefined; } isRunning(){ if (! this.valid()) return false; if (this.status === undefined) return false; return ['error','success','canceled','failed','errored'].indexOf(this.status) < 0; } } (function(){ const STATUS_INTERVAL=2000; const CURRENT_PIPELINE='pipeline'; const API="cibuild.php"; const GITAPI="install.php"; const GITUSER="wellenvogel"; const GITREPO="esp32-nmea2000"; let currentPipeline=new PipelineInfo(); let timer=undefined; let structure=undefined; let config={}; //values as read and stored let configStruct={}; //complete struct merged of config and struct let displayMode='last'; let delayedSearch=undefined; let gitSha=undefined; let buildVersion=undefined; let configName="buildconfig"; let isModified=false; const modeStrings={ last: 'Last Build', existing: 'Existing Build', current: 'Current Build' }; const setDisplayMode=(mode)=>{ let old=displayMode; let ms=modeStrings[mode]; if (ms === undefined){ return false; } displayMode=mode; setValue('resultTitle',ms); return mode !== old; } const updateStatus=()=>{ setValues(currentPipeline,{ id: 'pipeline' }); setVisible('download',currentPipeline.valid() && currentPipeline.downloadUrl!==undefined,true); setVisible('status_url',currentPipeline.valid() && currentPipeline.status_url!==undefined,true); setVisible('error',currentPipeline.error!==undefined,true); let values={}; fillValues(values,['configError','environment']); setVisible('buildCommand',values.environment !== "" && ! values.configError); if (values.configError) { enableEl('start',false); return; } if (!values.environment){ enableEl('start',false); return; } if (displayMode != 'existing'){ if (currentPipeline.valid()){ //check pipeline state if (['error','success','canceled','failed'].indexOf(currentPipeline.status) >= 0){ enableEl('start',true); return; } enableEl('start',false); return; } enableEl('start',true); return; } //display mode existing //allow start if either no pipeline or not running and status != success enableEl('start',!currentPipeline.valid() || (!currentPipeline.isRunning() && currentPipeline.status != "success")); } const isRunning=()=>{ return currentPipeline.isRunning(); } const fetchStatus=(initial)=>{ if (! currentPipeline.valid()){ updateStatus(); return; } let queryPipeline=currentPipeline.id; fetchJson(API,{api:'status',pipeline:currentPipeline.id}) .then((st)=>{ if (queryPipeline !== currentPipeline.id) return; let stid=st.pipeline_id||st.id; if (stid !== undefined && currentPipeline.id !== stid) return; if (st.status === undefined) st.status=st.state; currentPipeline.update(st); updateStatus(); if (st.status === 'error' || st.status === 'errored' || st.status === 'canceled'){ return; } if (st.status === 'success'){ fetchJson(API,{api:'artifacts',pipeline:currentPipeline.id}) .then((ar)=>{ if (! ar.items || ar.items.length < 1){ throw new Error("no download link"); } currentPipeline.downloadUrl=buildUrl(API,{ download: currentPipeline.id }); updateStatus(); }) .catch((err)=>{ currentPipeline.update({ status:'error', error:"Unable to get build result: "+err }); updateStatus(); }); return; } timer=window.setTimeout(fetchStatus,STATUS_INTERVAL) }) .catch((e)=>{ timer=window.setTimeout(fetchStatus,STATUS_INTERVAL); }) } const setCurrentPipeline=(pipeline,doStore)=>{ currentPipeline.reset(pipeline); if (doStore) window.localStorage.setItem(CURRENT_PIPELINE,pipeline); }; const startBuild=()=>{ let param={}; currentPipeline.reset(undefined,{status:'requested'}); if (timer) window.clearTimeout(timer); timer=undefined; fillValues(param,['environment','buildflags']); setDisplayMode('current'); updateStatus(); if (gitSha !== undefined) param.tag=gitSha; param.config=JSON.stringify(config); if (buildVersion !== undefined){ param.suffix="-"+buildVersion; } fetchJson(API,Object.assign({ api:'start'},param)) .then((json)=>{ let status=json.status || json.state|| 'error'; if (status === 'error'){ currentPipeline.update({status:status,error:json.error}) updateStatus(); throw new Error("unable to create job "+(json.error||'')); } if (!json.id) { let error="unable to create job, no id" currentPipeline.update({status:'error',error:error}); updateStatus(); throw new Error(error); } setCurrentPipeline(json.id,true); updateStatus(); timer=window.setTimeout(fetchStatus,STATUS_INTERVAL); }) .catch((err)=>{ currentPipeline.update({status:'error',error:err}); updateStatus(); }); } const runDownload=()=>{ if (! currentPipeline.downloadUrl) return; let df=document.getElementById('dlframe'); if (df){ df.setAttribute('src',null); df.setAttribute('src',currentPipeline.downloadUrl); } } const webInstall=()=>{ if (! currentPipeline.downloadUrl) return; let url=buildUrl("install.html",{custom:currentPipeline.downloadUrl}); window.location.href=url; } const uploadConfig=()=>{ let form=document.getElementById("upload"); form.reset(); let fsel=document.getElementById("fileSelect"); fsel.onchange=async ()=>{ if (fsel.files.length < 1) return; let file=fsel.files[0]; if (! file.name.match(/json$/)){ alert("only json files"); return; } try{ let content=await readFile(file,true); let newConfig=JSON.parse(content); removeSelectors(ROOT_PATH,true); config=newConfig; buildSelectors(ROOT_PATH,structure.config.children,true); findPipeline(); } catch (e){ alert("upload "+fsel.files[0].name+" failed: "+e); } } fsel.click(); } const downloadConfig=()=>{ let name=configName; if (isModified) name=name.replace(/[0-9]*$/,'')+formatDate(undefined,true); name+=".json"; fileDownload(JSON.stringify(config),name); } const showOverlay=(text, isHtml, secondButton)=>{ let el = document.getElementById('overlayContent'); if (isHtml) { el.innerHTML = text; el.classList.remove("text"); } else { el.textContent = text; el.classList.add("text"); } let container = document.getElementById('overlayContainer'); container.classList.remove('hidden'); let db=document.getElementById("secondDialogButton"); if (db) { if (secondButton && secondButton.callback) { db.classList.remove("hidden"); if (secondButton.title){db.textContent=secondButton.title} db.onclick=secondButton.callback; } else { db.classList.add("hidden"); } } } const hideOverlay=()=> { let container = document.getElementById('overlayContainer'); container.classList.add('hidden'); } const loadConfig=async (url)=>{ let config=await fetch(url).then((r)=>{ if (!r.ok) throw new Error("unable to fetch: "+r.statusText); return r.text() }); let parsed=yamlLoad(config); return parsed; } const showBuildCommand= async ()=>{ let v={}; fillValues(v,['environment','buildflags']); if (v.environment !== ""){ let help="Run the build from a command line:\n"; let cmd="PLATFORMIO_BUILD_FLAGS=\""; cmd+=v.buildflags; cmd+="\" pio run -e "+v.environment; help+=cmd; showOverlay(help,false,{title:"Copy",callback:(ev)=>{ try{ navigator.clipboard.writeText(cmd); //alert("copied:"+cmd); } catch (e){ alert("Unable to copy:"+e); } }}); } } const btConfig={ start:startBuild, download:runDownload, webinstall:webInstall, uploadConfig: uploadConfig, downloadConfig: downloadConfig, hideOverlay: hideOverlay, buildCommand: showBuildCommand }; const PATH_ATTR='data-path'; const SEPARATOR=':'; const expandObject=(obj,parent)=>{ if (typeof(obj) !== 'object'){ obj={value:obj} } let rt=Object.assign({},obj); if (rt.value === undefined && rt.key !== undefined) rt.value=rt.key; if (rt.key === undefined) rt.key=rt.value; if (rt.value === null) rt.value=undefined; if (rt.label === undefined){ if (rt.value !== undefined) rt.label=rt.value; else rt.label=rt.key; } if (rt.resource === undefined && typeof(parent) === 'object'){ if (parent.resource !== undefined){ if (parent.resource.match(/:$/)){ if(rt.value !== undefined && rt.value !== null){ rt.resource=parent.resource+rt.value; } } else{ rt.resource=parent.resource; } } } if (rt.target === undefined && typeof(parent) === 'object'){ rt.target=parent.target; } if (rt.mandatory === undefined && typeof(parent) === 'object'){ rt.mandatory=parent.mandatory; } return rt; } const expandList=(lst,parent)=>{ let rt=[]; if (! lst) return rt; lst.forEach((e)=>rt.push(expandObject(e,parent))); return rt; } const addDescription=(v,frame)=>{ if (frame === undefined) return; if (v.description){ if(v.url) { let lnk = addEl('a', 'description', frame, v.description); lnk.setAttribute('href', v.url); lnk.setAttribute('target', '_'); } else{ let de=addEl('div','description',frame,v.description); } } if (v.help){ let bt=addEl('button','help',frame,'?'); bt.addEventListener('click',()=>showOverlay(v.help)); } else if (v.helpHtml){ let bt=addEl('button','help',frame,'?'); bt.addEventListener('click',()=>showOverlay(v.helpHtml,true)); } } /** * * @param {build a selector} parent * @param {*} config * @param {*} name * @param {*} current * @param {*} callback will be called with: children,key,value,initial * @returns */ const buildSelector=(parent,cfgBase,name,current,callback)=>{ let config=expandObject(cfgBase); if (current === undefined && config.default !== undefined){ current=config.default; } let rep=new RegExp("[^"+SEPARATOR+"]*","g"); let level=name.replace(rep,''); let frame=addEl('div','selector level'+level.length+' t'+config.type,parent); frame.setAttribute(PATH_ATTR,name); let inputFrame=addEl('div','inputFrame',frame); let titleFrame=undefined; if (config.label !== undefined){ titleFrame=addEl('div','titleFrame t'+config.type,inputFrame); addEl('div','title t'+config.type,titleFrame,config.label); } let initialConfig=undefined if (config.type === 'frame' || config.type === undefined){ initialConfig=config; } let expandedValues=expandList(config.values,config); expandedValues.forEach((v)=>{ if (v.type !== undefined && v.type !== "frame"){ let err="value element with wrong type "+v.type+" at "+name; alert(err); throw new Error(err); } }) if (config.type === 'select') { addDescription(config,titleFrame); for (let idx=0;idx callback(v,false)); addDescription(v,ef); if (v.key == current) { re.setAttribute('checked','checked'); initialConfig=v; } }; } if (expandedValues.length > 0 && config.type === 'dropdown'){ let sel=addEl('select','t'+config.type,inputFrame); for (let idx=0;idx{ let v=expandedValues[ev.target.value]; if (! v) return; callback(v,false); }); } if (config.type === 'range'){ if (config.min !== undefined && config.max !== undefined) { let min=config.min+0; let step=1; if (config.step !== undefined) step=config.step+0; let max=config.max+0; let valid=false; if (step > 0){ if (min < max) valid=true; } else{ if (min > max) { let tmp=max; max=min; min=tmp; valid=true; } } if (! valid){ console.log("invalid range config",config); } else { let sel = addEl('select', 'tdropdown', inputFrame); for (let idx=min;idx <=max;idx+=step){ let opt=addEl('option','',sel,idx); opt.setAttribute('value',idx); if (idx == current){ opt.setAttribute('selected',true); initialConfig=expandObject({key:idx,value:idx},config); } } if (! initialConfig){ initialConfig=expandObject({key:min,value:min},config); } addDescription(config, inputFrame); sel.addEventListener('change', (ev) => { let v = expandObject({ key: ev.target.value, value: ev.target.value },config); callback(v, false); }); } } } if (expandedValues.length > 0 && config.type === 'checkbox'){ let act=undefined; let inact=undefined; expandedValues.forEach((ev)=>{ if (ev.key === true || ev.key === undefined){ act=ev; if (act.key === undefined) act.key=true; return; } inact=ev; }); if (act !== undefined){ if (inact === undefined) inact={key:false}; let cb=addEl('input','t'+config.type,inputFrame); cb.setAttribute('type','checkbox'); if (current) { cb.setAttribute('checked',true); initialConfig=act; } else{ initialConfig=inact; } addDescription(config,inputFrame); cb.addEventListener('change',(ev)=>{ if (ev.target.checked){ callback(act,false); } else { callback(inact,false); } }); } } if (expandedValues.length > 0 && config.type === 'display'){ let cb=addEl('div','t'+config.type,inputFrame); addDescription(config,inputFrame); initialConfig=expandedValues[0]; } let childFrame=addEl('div','childFrame',frame); if (initialConfig !== undefined){ callback(initialConfig,true,childFrame); } return childFrame; } const removeSelectors=(prefix,removeValues)=>{ forEachEl('.selectorFrame',(el)=>{ let path=el.getAttribute(PATH_ATTR); if (! path) return; if (path.indexOf(prefix) == 0){ el.remove(); } }) if (removeValues){ let removeKeys=[]; for (let k in configStruct){ if (k.indexOf(prefix) == 0) removeKeys.push(k); } for (let k in config){ if (k.indexOf(prefix) == 0) removeKeys.push(k); } removeKeys.forEach((k)=>{ delete config[k]; delete configStruct[k]; }); } } const buildSelectors=(prefix,configList,initial,base,parent)=>{ if (!parent) parent=document.getElementById("selectors");; if (!configList) return; let frame=addEl('div','selectorFrame',parent); frame.setAttribute(PATH_ATTR,prefix); let expandedList=expandList(configList); expandedList.forEach((cfg)=>{ let currentBase=Object.assign({},base,cfg.base); cfg=replaceValues(cfg,currentBase); if (cfg.key === undefined){ if (cfg.type !== undefined && cfg.type !== 'frame'){ console.log("config without key",cfg); return; } } let name=prefix; if (name !== undefined){ if (cfg.key !== undefined) { name=prefix+SEPARATOR+cfg.key; } } else{ name=cfg.key; } let current=config[name]; let childFrame=buildSelector(frame,cfg,name,current, (child,initial,opt_frame)=>{ if(cfg.key !== undefined) removeSelectors(name,!initial); if (! initial) isModified=true; buildSelectors(name,child.children,initial,Object.assign({},currentBase,child.base),opt_frame||childFrame); if (cfg.key !== undefined) configStruct[name]={cfg:child,base:currentBase}; buildValues(initial); }) }) } const replaceValues=(str,base)=>{ if (! base) return str; if (typeof(str) === 'string'){ for (let k in base){ if (typeof(base[k]) !== 'string'){ //special replacement //for complete parts if (str === '#'+k+'#'){ return base[k]; } } else{ let r=new RegExp("#"+k+"#","g"); str=str.replace(r,base[k]); } } return str; } if (str instanceof Array){ let rt=[]; str.forEach((el)=>{ rt.push(replaceValues(el,base)); }) return rt; } if (str instanceof Object){ let rt={}; for (let k in str){ if (k == 'children') rt[k]=str[k]; else rt[k]=replaceValues(str[k],base); } return rt; } return str; } const ROOT_PATH='root'; const buildValues=(initial)=>{ let environment; let flags=""; if (! initial){ config={}; } let allowedResources={}; let currentResources={}; let errors=""; for (let round = 0; round <= 1; round++) { //round1: find allowed resources //round2: really collect values for (let k in configStruct) { let container = configStruct[k]; if (! container.cfg) continue; let struct=container.cfg; if (round > 0) config[k] = struct.key; if (struct.target !== undefined ) { if (struct.value === undefined){ if (struct.mandatory && round > 0){ errors+=" missing value for "+k+"\n"; } continue; } let target=replaceValues(struct.target,container.base); if (target === 'environment' ) { if (round > 0 && struct.key !== undefined) environment = struct.value; else allowedResources=struct.resource; continue; } if (round < 1) continue; if (struct.resource){ let splitted=struct.resource.split(","); splitted.forEach((resource) => { let resList = currentResources[resource]; if (!resList) { resList = []; currentResources[resource] = resList; } resList.push(struct); }); } if (target === 'define') { flags += " -D" + struct.value; continue; } const DEFPRFX = "define:"; if (target.indexOf(DEFPRFX) == 0) { let def = target.substring(DEFPRFX.length); flags += " -D" + def + "=" + struct.value; continue; } } } } if (buildVersion !== undefined){ flags+=" -DGWRELEASEVERSION="+buildVersion; } setValues({environment:environment,buildflags:flags}); //check resources for (let k in currentResources){ let ak=k.replace(/:.*/,''); let resList=currentResources[k]; let allowed=allowedResources[ak]; if (allowed === undefined) allowed=1; if (resList.length > allowed){ errors+=" more than "+allowed+" device(s) of type "+k+" used"; } } if (errors){ setValue('configError',errors); setVisible('configError',true,true); } else{ setValue('configError',''); setVisible('configError',false,true); } if (! initial) findPipeline(); updateStatus(); } let findIdx=0; const findPipeline=()=>{ if (delayedSearch !== undefined){ window.clearTimeout(delayedSearch); delayedSearch=undefined; } if (isRunning()) { delayedSearch=window.setTimeout(findPipeline,500); return; } findIdx++; let queryIdx=findIdx; let param={find:1}; fillValues(param,['environment','buildflags']); if (gitSha !== undefined) param.tag=gitSha; fetchJson(API,param) .then((res)=>{ if (queryIdx != findIdx) return; setCurrentPipeline(res.pipeline); if (res.pipeline) currentPipeline.status="found"; setDisplayMode('existing'); updateStatus(); fetchStatus(true); }) .catch((e)=>{ console.log("findPipeline error ",e) if (displayMode == 'existing'){ setCurrentPipeline(); updateStatus(); } }); } const formatDate=(opt_date,opt_includeMs)=>{ const fmt=(v)=>{ return ((v<10)?"0":"")+v; } let now=opt_date|| new Date(); let rt=now.getFullYear()+fmt(now.getMonth()+1)+fmt(now.getDate()); if (opt_includeMs){ rt+=fmt(now.getHours())+fmt(now.getMinutes())+fmt(now.getSeconds()); } return rt; } window.onload=async ()=>{ setButtons(btConfig); let pipeline=window.localStorage.getItem(CURRENT_PIPELINE); setDisplayMode('last'); if (pipeline){ setCurrentPipeline(pipeline); updateStatus(); fetchStatus(true); } let gitParam={user:GITUSER,repo:GITREPO}; let branch=getParam('branch'); if (branch){ try{ let info=await fetchJson(GITAPI,Object.assign({},gitParam,{branch:branch})); if (info.object){ gitSha=info.object.sha; setValue('branchOrTag','branch'); setValue('branchOrTagValue',branch); } }catch (e){ console.log("branch query error",e); } } if (gitSha === undefined) { let tag = getParam('tag'); let type="tag"; if (!tag) { try { let relinfo = await fetchJson(GITAPI, Object.assign({}, gitParam, { api: 1 })); if (relinfo.tag_name) { tag = relinfo.tag_name; type="release"; } else { alert("unable to query latest release"); } } catch (e) { alert("unable to query release info " + e); } } if (tag){ try{ let info=await fetchJson(GITAPI,Object.assign({},gitParam,{tag:tag})); if (info.object){ gitSha=info.object.sha; setValue('branchOrTag',type); setValue('branchOrTagValue',tag); } }catch(e){ alert("cannot get sha for tag "+tag+": "+e); } } } if (gitSha === undefined){ //last resort: no sha, let the CI pick up latest setValue('gitSha','unknown'); setValue('branchOrTag','branch'); setValue('branchOrTagValue','master'); } else{ setValue('gitSha',gitSha); } let bot=document.getElementById('branchOrTag'); let botv=document.getElementById('branchOrTagValue'); if (bot && botv){ let type=bot.textContent; let val=botv.textContent; if (type && val){ if (type != 'release' && type != 'tag' && type != 'branch'){ val=type+val; } if (type == 'branch'){ val=val+formatDate(); } val=val.replace(/[:.]/g,'_'); val=val.replace(/[^a-zA-Z0-9_]*/g,''); if (val.length > 32){ val=val.substring(val.length-32) } if (val.length > 0){ buildVersion=val; setValue('buildVersion',buildVersion); } } } if (gitSha !== undefined){ let url=buildUrl(GITAPI,Object.assign({},gitParam,{sha:gitSha,proxy:'webinstall/build.yaml'})); try{ structure=await loadConfig(url); }catch (e){ alert("unable to load config for selected release:\n "+e+"\n falling back to default"); } } if (! structure){ structure=await loadConfig("build.yaml"); } let ucfg=getParam('config'); let loadedCfg=undefined; if (ucfg){ ucfg=ucfg.replace(/[^.a-zA-Z_0-9-]/g,''); if (gitSha !== undefined){ try{ loadedCfg=await fetchJson(GITAPI,Object.assign({},gitParam,{sha:gitSha,proxy:'webinstall/config/'+ucfg+".json"})); }catch(e){ alert("unable to load config "+ucfg+" for selected release, trying latest"); } } if (loadedCfg === undefined){ try{ loadedCfg=await fetchJson('config/'+ucfg+".json"); }catch(e){ alert("unable to load config "+ucfg+": "+e); } } if (loadedCfg !== undefined){ configName=ucfg; config=loadedCfg; } } buildSelectors(ROOT_PATH,structure.config.children,true); if (! isRunning()) findPipeline(); updateStatus(); const translationCheck=()=>{ const lang = document.documentElement.lang; if (lang != "en"){ alert( "This page will not work correctly with translation enabled" ); } } // Works at least for Chrome, Firefox, Safari and probably more. Not Microsoft // Edge though. They're special. // Yell at clouds if a translator doesn't change it const observer = new MutationObserver(() => { translationCheck(); }); observer.observe(document.documentElement, { attributes: true, attributeFilter: ['lang'], childList: false, characterData: false, }); translationCheck(); } })();