1 parent
e37d583179
commit
5260c2828b
65 files changed
+8469
-380
No files matched your search
+86
-34
@@ -1,13 +1,36 @@
|
||||
// Canvas graph is kept outside Alpine's reactive proxy. It never mutates scheduling state.
|
||||
function generationMapUi(){let graph=null,origin=null,request=0;return {
|
||||
function generationMapUi(){let graph=null,origin=null,request=0;const colors=new Map();return {
|
||||
mapProbe:null,mapProbeHistory:[],mapProbeStyle:{},
|
||||
mapBindingKey(b){if(!b)return '';return JSON.stringify([b.kind,b.variable||null,b.iteration||null,b.output||null,b.value??null,b.path||[],b.prefix||'',b.suffix||'',(b.items||[]).map(v=>this.mapBindingKey(v)),b.key?this.mapBindingKey(b.key):null]);},
|
||||
mapColor(key){if(!colors.has(key))colors.set(key,`hsl(${(colors.size*137.508)%360} 58% 32%)`);return colors.get(key);},
|
||||
mapAddressParts(address){const text=address.replace(/^\{\s*|\s*\}$/g,''),parts=[];let start=0,quote='',escape=false,depth=0;for(let i=0;i<text.length;i++){const c=text[i];if(quote){if(escape)escape=false;else if(c==='\\')escape=true;else if(c===quote)quote='';continue;}if(c==='"'||c==="'"){quote=c;continue;}if(c==='[')depth++;if(c===']')depth--;if(c==='.'&&depth===0){parts.push({text:text.slice(start,i),dot:false});parts.push({text:'.',dot:true});start=i+1;}}parts.push({text:text.slice(start),dot:false});return parts.map((p,i)=>({...p,color:this.mapColor(parts.slice(0,i+1).map(p=>p.text).join(''))}));},
|
||||
mapAllBindings(rule){const list=[];const walk=v=>{if(!v||typeof v!=='object')return;if(['literal','variable','item','key','lookup','output','list'].includes(v.kind)){list.push(v);if(v.key)walk(v.key);(v.items||[]).forEach(walk);return;}Object.values(v).forEach(walk);};walk(rule);return list;},
|
||||
mapAddressBinding(address){const rule=this.mapSnapshot.payload.rules.find(r=>r.id===this.mapCurrent()?.rule);return {rule:rule?.id,binding:this.mapAllBindings(rule).find(b=>'{ '+this.bindingAddress(b,0,rule.iterations)+' }'===address)};},
|
||||
mapTraceBinding(rule,binding,row){const key=this.mapBindingKey(binding),rows=this.mapTrace(rule)?.rows||[],found=[];for(const [index,pass] of rows.entries()){if(row!=null&&index!==row)continue;const scan=p=>{if(!p)return null;if(this.mapBindingKey(p.binding)===key)return p;for(const parent of p.parents||[]){const match=scan(parent);if(match)return match;}return null;};const trace=(pass.bindings||[]).map(scan).find(Boolean);if(trace)found.push({row:index,trace});}return found;},
|
||||
mapResourceOrigin(item,key){if(item.manual)return {manual:true,title:item.name,field:key,value:item.resource[key]};const rule=this.mapSnapshot.payload.rules.find(r=>r.id===item.rule),block=rule?.blocks.find(b=>b.id===item.block),row=this.mapTrace(item.rule)?.rows.findIndex(r=>r.resources.includes(item.id));let binding=block?.fields[key];const step=/^steps\.(\d+)\.(delay|recipient)$/.exec(key);if(step){const s=block.steps[Number(step[1])];binding=step[2]==='recipient'&&(s.team_all||s.team_random)?this.literal(tr(s.team_random?'a_random_team_member_8f35f3':'everyone_on_the_team_235b55')):s[step[2]];}const condition=/^conditions\.(\d+)$/.exec(key);if(condition)binding=block.conditions[Number(condition[1])]?.value;return {rule:item.rule,row:row>=0?row:null,binding,title:item.name,field:key,value:item.resource[key]};},
|
||||
mapNameParts(item){const origin=this.mapResourceOrigin(item,'name'),b=origin.binding;if(!b||b.kind==='literal')return [{text:item.name,origin,color:this.mapColor(this.mapBindingKey(b)||item.id)}];const prefix=b.prefix||'',suffix=b.suffix||'';return [{text:prefix,origin:{...origin,binding:this.literal(prefix)},color:this.mapColor('prefix')},{text:item.name.slice(prefix.length,suffix?item.name.length-suffix.length:undefined),origin,color:this.mapColor(this.mapBindingKey(b))},{text:suffix,origin:{...origin,binding:this.literal(suffix)},color:this.mapColor('suffix')}].filter(p=>p.text);},
|
||||
mapSampleElement(value,path){if(!path)return value;for(const key of path.split(' / ')){value=Array.isArray(value)?value[Number(key)-1]:value?.[key];}return value;},
|
||||
async mapExploreAddressPart(address,index,event){await this.mapExploreAddress(address,event);if(!this.mapProbe)return;const parts=this.mapAddressParts(address).filter(p=>!p.dot),selected=this.mapAddressParts(address).slice(0,index+1).filter(p=>!p.dot).length,stages=this.mapProbeStages();this.mapProbe.stage=selected===parts.length?Math.max(0,stages.length-1):Math.max(0,selected-(parts.length-stages.length)-1);},
|
||||
async mapExploreAddress(address,event){const source=this.mapAddressBinding(address);if(!source.binding)return;const row=this.mapRow==='all'?null:Number(this.mapRow);await this.mapExplore({...source,row,title:address},event);},
|
||||
async mapExplore(origin,event){const traces=origin.binding?this.mapTraceBinding(origin.rule,origin.binding,origin.row):[];const probe={...origin,traces,stage:null};if(this.mapProbe)this.mapProbeHistory.push(this.mapProbe);else this.mapProbeHistory=[];this.mapProbe=probe;const rect=event?.currentTarget?.getBoundingClientRect()||{left:innerWidth/2,top:80};this.mapProbeStyle={left:Math.max(12,Math.min(rect.left,innerWidth-402))+'px',top:Math.max(12,Math.min(rect.top,innerHeight-570))+'px'};await this.$nextTick();const popup=document.querySelector('.generation-map .map-probe');if(this.mapOpen && this.mapProbe && popup && !popup.matches(':popover-open'))popup.showPopover();},
|
||||
mapProbeClose(){const el=document.querySelector('.generation-map .map-probe');if(el?.matches(':popover-open'))el.hidePopover();this.mapProbe=null;this.mapProbeHistory=[];},
|
||||
mapProbeBack(){if(this.mapProbeHistory.length)this.mapProbe=this.mapProbeHistory.pop();},
|
||||
mapProbeAddress(){const p=this.mapProbe,rule=this.mapSnapshot?.payload.rules.find(r=>r.id===p?.rule);return p?.binding?'{ '+this.bindingAddress(p.binding,0,rule?.iterations||[])+' }':p?.field||'';},
|
||||
mapProbeStages(){return this.mapProbe?.traces[0]?.trace.stages||[];},
|
||||
mapProbeValues(){const p=this.mapProbe;if(!p)return [];if(!p.traces.length){return [{label:tr(p.manual?'map_manual':p.binding?.kind==='literal'?'fixed_value_191429':'map_no_trace'),value:p.binding?.kind==='literal'?p.binding.value:p.value}];}return p.traces.slice(0,20).map(({row,trace})=>{const result=p.stage==null?trace.result:trace.stages[p.stage]?.result;return {label:tr('map_pass_number',{number:row+1}),value:this.mapSampleElement(result?.value,p.stage==null?p.elementPath:null),error:result?.error};});},
|
||||
mapProbeOutputSources(){const b=this.mapProbe?.binding;if(b?.kind!=='output')return [];const rule=this.mapSnapshot.payload.rules.find(r=>r.blocks.some(block=>block.id===b.output)),block=rule?.blocks.find(block=>block.id===b.output);return block?Object.entries(block.fields).map(([field,binding])=>({title:this.fieldsFor(block.kind).find(f=>f.key===field)?.label||field,binding,rule:rule.id,row:rule.id===this.mapProbe.rule?this.mapProbe.row:null})):[];},
|
||||
mapProbeParents(){return this.mapProbe?.traces[0]?.trace.parents||[];},
|
||||
mapProbeFollow(trace,event){this.mapExplore({rule:this.mapProbe.rule,row:this.mapProbe.row,binding:trace.binding,title:tr('map_source_chain')},event);},
|
||||
mapProbeTarget(){const b=this.mapProbe?.binding;if(!b)return '';if(b.variable)return 'v:'+b.variable;if(b.iteration)return 'i:'+b.iteration;if(b.output)return 'b:'+b.output;if(b.kind==='literal'&&this.mapNodes.some(n=>n.id==='m:'+b.value))return 'm:'+b.value;return '';},
|
||||
mapExploreToken(item,field,token,event){const origin=this.mapResourceOrigin(item,field.key);if(!origin.binding)origin.value=field.value;const index=Number(token.path?.split(' / ')[0])-1;if(origin.binding?.kind==='list' && origin.binding.items[index])origin.binding=origin.binding.items[index];else origin.elementPath=token.path;this.mapExplore(origin,event);},
|
||||
mapOpen:false,mapBusy:false,mapError:'',mapSnapshot:null,mapNodes:[],mapSelected:'',mapExpanded:{},mapRow:'all',
|
||||
async openGenerationMap(){origin=document.activeElement;this.mapOpen=true;this.mapBusy=true;this.mapSnapshot=null;this.mapNodes=[];this.mapSelected='';this.mapExpanded={};this.mapRow='all';await this.$nextTick();this.$refs.generationMap.showModal();await this.refreshGenerationMap();},
|
||||
closeGenerationMap(){request++;graph?.destroy();graph=null;this.$refs.generationMap.close();this.mapOpen=false;origin?.focus();},
|
||||
async refreshGenerationMap(){const sequence=++request;this.mapBusy=true;this.mapError='';try{const payload=this.generationPayload(),workspace=this.workspace.id;const report=await this.api(this.endpoint('/generation-preview'),'POST',payload);if(sequence!==request || !this.mapOpen || workspace!==this.workspace.id)return;this.mapSnapshot={payload,report};await this.$nextTick();this.drawGenerationMap();}catch(e){if(sequence===request)this.mapError=e.message;}finally{if(sequence===request)this.mapBusy=false;}},
|
||||
closeGenerationMap(){this.mapProbeClose();request++;graph?.destroy();graph=null;this.$refs.generationMap.close();this.mapOpen=false;origin?.focus();},
|
||||
async refreshGenerationMap(){const sequence=++request;this.mapBusy=true;this.mapError='';try{const payload=this.generationPayload(),workspace=this.workspace.id,document=copy(this.doc);const report=await this.api(this.endpoint('/generation-preview'),'POST',payload);if(sequence!==request || !this.mapOpen || workspace!==this.workspace.id)return;this.mapSnapshot={payload,report,document};await this.$nextTick();this.drawGenerationMap();}catch(e){if(sequence===request)this.mapError=e.message;}finally{if(sequence===request)this.mapBusy=false;}},
|
||||
mapSelect(id){this.mapSelected=id;this.mapRow='all';if(graph){graph.elements().removeClass('selected related');const n=graph.getElementById(id);n.addClass('selected');n.closedNeighborhood().addClass('related');}},
|
||||
mapCurrent(){return this.mapNodes.find(n=>n.id===this.mapSelected);},
|
||||
mapTrace(rule){return this.mapSnapshot?.report.trace?.find(t=>t.rule===rule);},
|
||||
mapResources(){const n=this.mapCurrent(),report=this.mapSnapshot?.report;if(this.mapBusy || this.mapError || !n || n.variable || !report?.result)return [];const row=this.mapRow==='all'?null:this.mapTrace(n.rule)?.rows[Number(this.mapRow)];return report.result.managed.filter(m=>(!n.rule || m.rule===n.rule)&&(!n.block || m.block===n.block)&&(!row || row.resources.includes(m.id))).map(m=>{const kind=['rotation','priority'].includes(m.kind)?'schedules':m.kind==='escalation'?'escalations':'routes';const resource=report.result[kind].find(r=>r.id===m.id);return {...m,resource,name:resource?.name||m.id};});},
|
||||
mapResources(){const n=this.mapCurrent(),report=this.mapSnapshot?.report;if(this.mapBusy || this.mapError || !n || n.variable)return [];if(n.manual)return [{id:n.resourceId,kind:n.manualKind,resource:n.source,name:n.source.name,manual:true}];if(!report?.result)return [];const row=this.mapRow==='all'?null:this.mapTrace(n.rule)?.rows[Number(this.mapRow)];return report.result.managed.filter(m=>(!n.rule || m.rule===n.rule)&&(!n.block || m.block===n.block)&&(!n.resourceId || m.id===n.resourceId)&&(!row || row.resources.includes(m.id))).map(m=>{const kind=['rotation','priority'].includes(m.kind)?'schedules':m.kind==='escalation'?'escalations':'routes';const resource=report.result[kind].find(r=>r.id===m.id);return {...m,resource,name:resource?.name||m.id};});},
|
||||
mapIssues(){const n=this.mapCurrent();return (this.mapSnapshot?.report.issues||[]).filter(i=>!n || (n.variable?i.variable_id===n.variable:!i.rule_id || i.rule_id===n.rule)&&(!n.block || !i.block_id || i.block_id===n.block));},
|
||||
mapRows(){return this.mapTrace(this.mapCurrent()?.rule)?.rows||[];},
|
||||
mapRowLabel(row,index){const rule=this.mapSnapshot.payload.rules.find(r=>r.id===this.mapCurrent()?.rule);const names=(rule?.iterations||[]).map((iteration,i)=>{const v=row.inputs?.[iteration.id];return typeof v==='object'&&v!==null?(v.name||v.title||row.labels?.[i]||iteration.name):String(v??iteration.name);});return (index+1)+'. '+(names.join(' / ')||tr('map_once'))+' · '+tr('map_'+row.status,{count:1});},
|
||||
@@ -22,8 +45,8 @@ function generationMapUi(){let graph=null,origin=null,request=0;return {
|
||||
const producer=payload.rules.find(r=>r.blocks.some(b=>b.id===value.output)),block=producer?.blocks.find(b=>b.id===value.output);
|
||||
if(block && (n.block || producer.id!==n.rule)){const managed=report.result?.managed.filter(m=>m.block===block.id)||[],resources=[...(report.result?.schedules||[]),...(report.result?.escalations||[]),...(report.result?.routes||[])],resource=managed.length===1?resources.find(r=>r.id===managed[0].id):null,key='o:'+block.id;const target=this.mapNodes.find(v=>v.block===block.id)||this.mapNodes.find(v=>v.id==='g:'+producer.id)||this.mapNodes.find(v=>v.id==='r:'+producer.id);sources.set(key,{id:key,name:resource?.name||producer.name,type:['rotation','priority'].includes(block.kind)?tr('resource_schedule'):this.label(block.kind),count:managed.length,generated:true,addresses:[],target:target?.id});}
|
||||
}
|
||||
if(value.kind==='literal'&&typeof value.value==='string'){const id=value.value,resource=[...this.doc.schedules,...this.doc.escalations,...this.doc.routes].find(r=>r.id===id);if(resource)sources.set('saved:'+id,{id:'saved:'+id,name:resource.name,type:tr('source_saved_resource',{name:''}),addresses:[]});}
|
||||
Object.values(value).forEach(child=>scan(child,owner,depth+1));
|
||||
if(value.kind==='literal'&&typeof value.value==='string'){const id=value.value,resource=[...this.doc.schedules,...this.doc.escalations,...this.doc.routes].find(r=>r.id===id);if(resource)sources.set('saved:'+id,{id:'saved:'+id,name:resource.name,type:tr('source_saved_resource',{name:''}),addresses:[],target:'m:'+id});}
|
||||
Object.entries(value).forEach(([key,child])=>{if(key!=='recipient'||!(value.team_all||value.team_random))scan(child,owner,depth+1);});
|
||||
};
|
||||
if(n.variable)variable(n.variable);else if(n.kind==='iteration')scan(n.source.source);else if(n.kind==='condition')scan(rule?.when);else if(rule){if(!n.block)scan(rule.iterations);scan(rule.blocks.filter(b=>!n.block||b.id===n.block));if(!n.block)scan(rule.when);}
|
||||
return [...sources.values()];
|
||||
@@ -37,7 +60,7 @@ function generationMapUi(){let graph=null,origin=null,request=0;return {
|
||||
if(n.kind==='condition')return [{title:tr('map_condition'),rows:[{label:tr('map_condition_left'),values:unique(rows.map(r=>r.condition?.left))},{label:tr('map_condition_operation'),values:[this.label(rule.when.operation)]},{label:tr('map_condition_right'),values:unique(rows.map(r=>r.condition?.right))}]}];
|
||||
return rule.blocks.filter(b=>!n.block||n.block===b.id).map(block=>{
|
||||
const inputs=rows.flatMap(r=>(r.blocks||[]).filter(b=>b.block===block.id)),fields=this.fieldsFor(block.kind),out=[];
|
||||
for(const field of fields){const values=unique(inputs.map(b=>b.fields[field.key]));if(values.length)out.push({label:field.label,values});}
|
||||
for(const field of fields){const values=unique(inputs.map(b=>b.fields[field.key]));if(values.length)out.push({label:field.label,values,binding:block.fields[field.key]});}
|
||||
const maxSteps=Math.max(0,...inputs.map(b=>b.steps.length));for(let i=0;i<maxSteps;i++){out.push({label:tr('map_step_delay',{step:i+1}),values:unique(inputs.map(b=>b.steps[i]?.delay))},{label:tr('map_step_recipient',{step:i+1}),values:unique(inputs.map(b=>b.steps[i]?.recipient))});}
|
||||
const conditions=unique(inputs.flatMap(b=>b.conditions));for(const c of conditions)out.push({label:this.label(c.field)+' · '+(c.negate?tr('map_not')+' ':'')+this.label(c.operation),values:[c.value]});
|
||||
if(block.kind==='route'&&inputs.length&&!conditions.length)out.push({label:tr('map_condition'),values:[tr('map_matches_all')]});
|
||||
@@ -45,20 +68,18 @@ function generationMapUi(){let graph=null,origin=null,request=0;return {
|
||||
}).filter(section=>section.rows.length);
|
||||
},
|
||||
mapResourceFields(item){
|
||||
const r=item.resource;if(!r)return [];const fields=[],add=(key,value)=>fields.push({label:tr(key),value});
|
||||
const r=item.resource;if(!r)return [];const fields=[],add=(key,label,value,hint='')=>fields.push({key,label:tr(label),value,hint});
|
||||
if(['rotation','priority'].includes(item.kind)){
|
||||
add(item.kind==='priority'?'responsible_people_in_priority_order_7205a5':'participants_in_rotation_order_4edee0',r.participants);
|
||||
if(item.kind==='priority')add('fallback_schedule_7b7a21',r.fallback);
|
||||
else {add('anchor_date_e77bd6',this.formatDate(r.anchor));add('shift_length_days_0b28fa',r.shift_days);}
|
||||
add('handover_time_c9e315',r.handover.slice(0,5)+' · '+this.mapSnapshot.report.result.timezone);
|
||||
add('participants',item.kind==='priority'?'responsible_people_in_priority_order_7205a5':'participants_in_rotation_order_4edee0',r.participants);
|
||||
if(item.kind==='priority')add('fallback','fallback_schedule_7b7a21',r.fallback);
|
||||
else {add('anchor','anchor_date_e77bd6',this.formatDate(r.anchor));add('shift_days','shift_length_days_0b28fa',r.shift_days);}
|
||||
add('handover','handover_time_c9e315',r.handover.slice(0,5));
|
||||
}else if(item.kind==='escalation'){
|
||||
r.steps.forEach((step,index)=>{fields.push({label:tr('map_step_recipient',{step:index+1}),value:step.recipient.id||(step.recipient.type==='team_all'?tr('everyone_on_the_team_235b55'):tr('a_random_team_member_8f35f3')),hint:(step.delay_minutes===0?tr('immediately_21b60a'):step.delay_minutes+tr('min_after_alert_8adeb2'))+' · '+tr(step.condition==='if-not-acked'?'if_unacknowledged_407d03':'if_still_open_c34949')});});
|
||||
add('repeat_count_912e08',r.repeat.count&&r.repeat.wait_minutes?tr('repeat_654579')+r.repeat.count+tr('times_wait_e1b163')+r.repeat.wait_minutes+tr('min_f4e8a8'):tr('end_of_policy_no_repeats_c2ac4f'));
|
||||
if(r.repeat.count&&r.repeat.wait_minutes){add('reset_recipient_states_on_repeat_8152c6',r.repeat.reset_recipient_states);add('close_alert_after_all_repeats_2bec22',r.repeat.close_alert_after_all);}
|
||||
r.steps.forEach((step,i)=>{add('steps.'+i+'.recipient','escalation_recipient_9dfd31',step.recipient.id||(step.recipient.type==='team_all'?tr('everyone_on_the_team_235b55'):tr('a_random_team_member_8f35f3')),(step.delay_minutes===0?tr('immediately_21b60a'):step.delay_minutes+tr('min_after_alert_8adeb2'))+' · '+tr(step.condition==='if-not-acked'?'if_unacknowledged_407d03':'if_still_open_c34949'));add('steps.'+i+'.delay','delay_in_minutes_e18c95',step.delay_minutes);});
|
||||
add('repeat_count','repeat_count_912e08',r.repeat.count);add('repeat_wait','minutes_between_repeats_f17ab0',r.repeat.wait_minutes);
|
||||
}else if(item.kind==='route'){
|
||||
add('resource_all_conditions',r.conditions.length?r.conditions.map(c=>this.conditionText(c)):tr('any_alert_4496c3'));
|
||||
if(r.window){const w=r.window,days=['mon_24b2a0','tue_529541','wed_23408b','thu_3593cc','fri_bbd6e3','sat_6b782d','sun_48c98c'];add('workspace_working_interval_c7d250',w.weekdays.map(d=>tr(days[d-1])).join(', ')+' · '+w.start.slice(0,5)+'–'+w.end.slice(0,5)+' · '+w.timezone);}
|
||||
add('resource_detail_destination',r.escalation_id||tr('do_not_notify_1a59a6'));
|
||||
r.conditions.forEach((c,i)=>add('conditions.'+i,'value_8dce17',c.value,this.conditionText(c)));
|
||||
add('working','only_during_working_hours_fa1eb6',!!r.window);add('escalation','resource_detail_destination',r.escalation_id||tr('do_not_notify_1a59a6'));
|
||||
}
|
||||
return fields;
|
||||
},
|
||||
@@ -69,27 +90,58 @@ function generationMapUi(){let graph=null,origin=null,request=0;return {
|
||||
const tokens=[];const visit=(v,path='',depth=0)=>{if(tokens.length>=80||depth>10)return;if(typeof v==='string'&&refs.has(v)){tokens.push({...refs.get(v),path});return;}if(Array.isArray(v)){if(!v.length)tokens.push({text:tr('map_empty_value'),path});else v.forEach((item,i)=>visit(item,path?path+' / '+(i+1):String(i+1),depth+1));return;}if(v&&typeof v==='object'){Object.entries(v).forEach(([key,item])=>visit(item,path?path+' / '+key:key,depth+1));return;}tokens.push({path,text:v===null?tr('map_missing_value'):typeof v==='boolean'?tr(v?'true_88b33e':'false_97cdbd'):typeof v==='string'&&/^[0-9a-f]{8}-[0-9a-f-]{27}$/i.test(v)?tr('map_missing_reference'):String(v)});};visit(value);return tokens;
|
||||
},
|
||||
mapToggle(){const n=this.mapCurrent();if(!n?.rule)return;this.mapExpanded[n.rule]=!this.mapExpanded[n.rule];this.drawGenerationMap();},
|
||||
mapFit(){graph?.fit(undefined,45);},mapZoom(factor){if(graph)graph.zoom({level:Math.max(.2,Math.min(2,graph.zoom()*factor)),renderedPosition:{x:graph.width()/2,y:graph.height()/2}});},
|
||||
async mapEdit(){const n=this.mapCurrent();if(!n)return;this.closeGenerationMap();await this.openIssue(n.variable?{kind:'variable',id:n.variable}:{kind:'rule',id:n.rule,block:n.block});},
|
||||
mapFit(){if(!graph)return;graph.resize();graph.fit(undefined,35);const box=graph.elements().boundingBox(),zoom=graph.zoom();graph.pan({x:35-box.x1*zoom,y:35-box.y1*zoom});},mapZoom(factor){if(graph)graph.zoom({level:Math.max(.2,Math.min(2,graph.zoom()*factor)),renderedPosition:{x:graph.width()/2,y:graph.height()/2}});},
|
||||
async mapEdit(){const n=this.mapCurrent();if(!n)return;if(n.manual){this.closeGenerationMap();this.open(n.manualKind==='escalation'?'policy':n.manualKind,n.source);return;}this.closeGenerationMap();await this.openIssue(n.variable?{kind:'variable',id:n.variable}:{kind:'rule',id:n.rule,block:n.block});},
|
||||
drawGenerationMap(){
|
||||
if(!this.mapSnapshot || !this.mapOpen)return;graph?.destroy();const {payload,report}=this.mapSnapshot,nodes=[],edges=[],ids=new Set(),blockTargets=new Map();
|
||||
const add=(id,title,subtitle,kind,x,y,meta={})=>{ids.add(id);nodes.push({id,title,subtitle,kind,...meta,position:{x,y}});};
|
||||
const edge=(source,target,role='flow')=>{if(source!==target)edges.push({data:{id:role+':'+source+'>'+target,source,target,role}});};
|
||||
payload.variables.forEach((v,i)=>add('v:'+v.id,v.name,this.nodeSummary(v.node),'variable',i*300,-160,{variable:v.id,source:v.node}));
|
||||
for(const [ruleIndex,rule] of payload.rules.entries()){const trace=this.mapTrace(rule.id),count=report.result?.managed.filter(m=>m.rule===rule.id).length||0,iterated=rule.iterations.length>0;const x=ruleIndex*350,y=0;
|
||||
const issue=report.issues.some(i=>i.rule_id===rule.id),status=!rule.enabled?tr('disabled_f4f447'):issue?tr('map_invalid'):!report.result?tr('map_unverified'):tr('map_count',{count});
|
||||
add('r:'+rule.id,rule.name,status,!rule.enabled?'disabled':issue?'error':'rule',x,y,{rule:rule.id,source:rule,step:ruleIndex+1});
|
||||
if(ruleIndex)edge('r:'+payload.rules[ruleIndex-1].id,'r:'+rule.id,'order');
|
||||
const references=value=>{const out=[];const walk=v=>{if(!v||typeof v!=='object')return;if(v.kind==='variable'||v.kind==='lookup')out.push('v:'+v.variable);if(v.kind==='output')out.push('b:'+v.output);if(v.kind==='literal'&&typeof v.value==='string')out.push('m:'+v.value);if(v.kind==='literal')return;Object.entries(v).forEach(([key,child])=>{if(key!=='recipient'||!(v.team_all||v.team_random))walk(child);});};walk(value);return [...new Set(out)];};
|
||||
const firstUse=new Map();payload.rules.forEach((r,i)=>references(r).forEach(id=>{if(!firstUse.has(id))firstUse.set(id,i);}));
|
||||
const variables=[...payload.variables].sort((a,b)=>(firstUse.get('v:'+a.id)??Infinity)-(firstUse.get('v:'+b.id)??Infinity));
|
||||
const columns=[],varX=new Map();let cursor=0;
|
||||
payload.rules.forEach((r,i)=>{const vars=variables.filter(v=>firstUse.get('v:'+v.id)===i),width=Math.max(300,vars.length*260);columns.push(cursor+width/2);vars.forEach((v,j)=>varX.set(v.id,cursor+130+j*260));cursor+=width+40;});
|
||||
variables.filter(v=>!varX.has(v.id)).forEach(v=>{varX.set(v.id,cursor+130);cursor+=280;});
|
||||
const resourceY=420+Math.max(0,...payload.rules.map(r=>r.iterations.length+(r.when?1:0)))*90;
|
||||
for(const v of variables)add('v:'+v.id,v.name,this.nodeSummary(v.node),'variable',varX.get(v.id),40,{variable:v.id,source:v.node,lane:0});
|
||||
const resources=report.result||this.mapSnapshot.document,managed=report.result?.managed||[],all=[...resources.schedules,...resources.escalations,...resources.routes];
|
||||
for(const [i,rule] of payload.rules.entries()){
|
||||
const x=columns[i],trace=this.mapTrace(rule.id),count=managed.filter(m=>m.rule===rule.id).length,issue=report.issues.some(v=>v.rule_id===rule.id);
|
||||
add('r:'+rule.id,rule.name,!rule.enabled?tr('disabled_f4f447'):issue?tr('map_invalid'):tr('map_count',{count}),'rule',x,240,{rule:rule.id,source:rule,step:i+1,lane:1,disabled:!rule.enabled,error:issue});
|
||||
if(i)edge('r:'+payload.rules[i-1].id,'r:'+rule.id,'order');
|
||||
let parent='r:'+rule.id;
|
||||
if(iterated){for(const [k,iteration] of rule.iterations.entries()){const id='i:'+iteration.id;add(id,iteration.name+' · '+tr('map_loop',{count:trace?.stages.find(s=>s.iteration===iteration.id)?.count||0}),'{ '+this.bindingAddress(iteration.source,0,rule.iterations)+' }','iteration',x,120+k*110,{rule:rule.id,source:iteration,iteration:true});edge(parent,id);parent=id;}}
|
||||
const outputY=120+rule.iterations.length*110;
|
||||
if(rule.when){const id='c:'+rule.id;add(id,tr('map_condition'),tr('map_filtered',{count:trace?.rows.filter(r=>r.status==='filtered').length||0}),'condition',x,outputY,{rule:rule.id,source:rule.when});edge(parent,id);parent=id;}
|
||||
if(iterated&&!this.mapExpanded[rule.id]){const id='g:'+rule.id;add(id,tr('map_group'),report.result?tr('map_count',{count}):tr('map_unverified'),'batch',x,outputY+(rule.when?110:0),{rule:rule.id,source:rule.blocks,iteration:true});edge(parent,id);parent=id;}
|
||||
for(const [j,b] of rule.blocks.entries()){if(iterated&&!this.mapExpanded[rule.id]){blockTargets.set(b.id,parent);continue;}const count=report.result?.managed.filter(m=>m.block===b.id).length||0;const id='b:'+b.id;blockTargets.set(b.id,id);const produced=report.result?.managed.filter(m=>m.block===b.id)||[],resource=produced.length===1?[...report.result.schedules,...report.result.escalations,...report.result.routes].find(r=>r.id===produced[0].id):null;const kind=['rotation','priority'].includes(b.kind)?tr('schedule_type_named',{name:this.label(b.kind)}):this.label(b.kind);add(id,resource?resource.name:kind,report.result?(resource?kind:tr('map_count',{count})):tr('map_unverified'),iterated?'batch':'output',x,outputY+(rule.when?110:0)+j*110,{rule:rule.id,block:b.id,source:b});edge(parent,id);parent=id;}
|
||||
rule.iterations.forEach((it,j)=>{const id='i:'+it.id;add(id,it.name,tr('map_loop',{count:trace?.stages.find(s=>s.iteration===it.id)?.count||0}),'iteration',x,335+j*90,{rule:rule.id,source:it,iteration:true,lane:1});edge(parent,id);parent=id;});
|
||||
if(rule.when){const id='c:'+rule.id;add(id,tr('map_condition'),tr('map_filtered',{count:trace?.rows.filter(r=>r.status==='filtered').length||0}),'condition',x,335+rule.iterations.length*90,{rule:rule.id,source:rule.when,lane:1});edge(parent,id);}
|
||||
let row=0;
|
||||
for(const block of rule.blocks){
|
||||
const produced=managed.filter(m=>m.block===block.id),id='b:'+block.id,kind=['rotation','priority'].includes(block.kind)?'schedule':block.kind;
|
||||
blockTargets.set(block.id,id);
|
||||
const single=produced.length===1?all.find(r=>r.id===produced[0].id):null;
|
||||
add(id,single?.name||this.label(block.kind),report.result?tr('map_count',{count:produced.length}):tr('map_unverified'),'output',x,resourceY+row++*100,{rule:rule.id,block:block.id,source:block,lane:2,resourceKind:kind,iteration:produced.length>1});edge('r:'+rule.id,id);
|
||||
if(this.mapExpanded[rule.id]&&produced.length>1)for(const m of produced){const resource=all.find(r=>r.id===m.id);add('o:'+m.id,resource?.name||m.id,this.label(block.kind),'output',x,resourceY+row++*100,{rule:rule.id,block:block.id,resourceId:m.id,source:block,lane:2,resourceKind:kind});edge(id,'o:'+m.id);}
|
||||
}
|
||||
}
|
||||
const scan=(value,target)=>{if(!value||typeof value!=='object')return;if(value.kind==='variable'||value.kind==='lookup'){if(ids.has('v:'+value.variable))edge('v:'+value.variable,target,'dependency');}if(value.kind==='output'&&blockTargets.has(value.output))edge(blockTargets.get(value.output),target,'dependency');Object.values(value).forEach(v=>scan(v,target));};
|
||||
for(const rule of payload.rules){rule.iterations.forEach(i=>scan(i.source,'i:'+i.id));if(rule.when)scan(rule.when,'c:'+rule.id);rule.blocks.forEach(b=>scan(b,blockTargets.get(b.id)));}
|
||||
const sheets=nodes.filter(n=>n.kind==='batch').flatMap(n=>[2,1].map(layer=>({data:{id:n.id+':sheet:'+layer,owner:n.id,layer,kind:'sheet'},classes:'stack-sheet',position:{x:n.position.x+layer*8,y:n.position.y-layer*8},selectable:false,grabbable:false})));
|
||||
this.mapNodes=nodes;graph=cytoscape({container:this.$refs.generationCanvas,elements:[...sheets,...nodes.map(n=>({data:{id:n.id,label:(n.step?n.step+'. ':'')+n.title+'\n'+n.subtitle,kind:n.kind},position:n.position})),...Array.from(new Map(edges.filter(e=>ids.has(e.data.source)&&ids.has(e.data.target)).map(e=>[e.data.id,e])).values())],layout:{name:'preset'},minZoom:.2,maxZoom:2,wheelSensitivity:.2,style:[{selector:'node',style:{shape:'round-rectangle',width:220,height:66,'background-color':'#fff','border-color':'#cad9cf','border-width':1.5,label:'data(label)','text-wrap':'wrap','text-max-width':195,'text-valign':'center',color:'#234236','font-family':'system-ui','font-size':11,'line-height':1.6,'z-index-compare':'manual','z-index':5}},{selector:'node[kind="variable"]',style:{'background-color':'#f4f0e7','border-color':'#d8c9aa'}},{selector:'node[kind="iteration"],node[kind="batch"]',style:{'background-color':'#edf2fb','border-color':'#94add2','border-style':'dashed'}},{selector:'.stack-sheet',style:{label:'','background-color':'#f4f7fc','border-color':'#94add2','border-style':'solid','border-width':1.5,'z-index':1,events:'no'}},{selector:'node[kind="batch"]',style:{'border-style':'solid','border-width':2}},{selector:'node[kind="condition"]',style:{'background-color':'#fff5df','border-color':'#d4b773'}},{selector:'node[kind="rule"]',style:{'background-color':'#e8f3ec','border-color':'#79a68a'}},{selector:'node[kind="disabled"]',style:{opacity:.5}},{selector:'node[kind="error"]',style:{'background-color':'#fcece7','border-color':'#ce7863'}},{selector:'edge',style:{width:1.5,'line-color':'#a4b6ab','target-arrow-color':'#839e8e','target-arrow-shape':'triangle','curve-style':'bezier'}},{selector:'edge[role="order"]',style:{width:3,'line-color':'#428465','target-arrow-color':'#428465','curve-style':'straight','z-index':3}},{selector:'edge[role="dependency"]',style:{'line-style':'dashed',opacity:.2,'curve-style':'unbundled-bezier','control-point-distances':-45,'control-point-weights':.5}},{selector:'edge.related',style:{opacity:1}},{selector:'.related',style:{'border-color':'#428465','line-color':'#428465','target-arrow-color':'#428465'}},{selector:'node.selected',style:{'border-width':3,'border-color':'#27694a'}}]});graph.on('tap','node:not(.stack-sheet)',e=>this.mapSelect(e.target.id()));graph.on('position','node[kind="batch"]',e=>{const p=e.target.position();for(const layer of [1,2])graph.getElementById(e.target.id()+':sheet:'+layer).position({x:p.x+layer*8,y:p.y-layer*8});});graph.fit(undefined,45);if(!ids.has(this.mapSelected))this.mapSelected=nodes.find(n=>n.rule)?.id||nodes[0]?.id||'';this.mapSelect(this.mapSelected);
|
||||
const manual=all.filter(r=>!(resources.managed||resources.generation?.managed||[]).some(m=>m.id===r.id));
|
||||
manual.forEach((r,i)=>{const kind=resources.schedules.includes(r)?r.type:resources.escalations.includes(r)?'escalation':'route';add('m:'+r.id,r.name,tr('map_manual'),'output',cursor+150,resourceY+i*100,{manual:true,resourceId:r.id,source:r,lane:2,resourceKind:['priority','rotation'].includes(kind)?'schedule':kind,manualKind:kind});});
|
||||
payload.rules.forEach(rule=>{for(const ref of references(rule)){const target=ref.startsWith('b:')?blockTargets.get(ref.slice(2)):ref;if(target)edge(target,'r:'+rule.id,'dependency');}});
|
||||
const maxX=Math.max(400,...nodes.map(n=>n.position.x))+170,minX=-180;
|
||||
const guides=[];for(const [i,y] of [140,resourceY-60].entries()){guides.push({data:{id:'line-left-'+i,kind:'guide',label:''},position:{x:minX,y},selectable:false,grabbable:false},{data:{id:'line-right-'+i,kind:'guide',label:''},position:{x:maxX,y},selectable:false,grabbable:false},{data:{id:'line-'+i,source:'line-left-'+i,target:'line-right-'+i,role:'boundary'}});}
|
||||
[ ['map_variables',40],['map_generators',240],['map_resource_lane',resourceY] ].forEach(([key,y],i)=>guides.push({data:{id:'lane-'+i,kind:'lane',label:tr(key)},position:{x:minX,y},selectable:false,grabbable:false}));
|
||||
this.mapNodes=nodes;graph=cytoscape({container:this.$refs.generationCanvas,elements:[...guides,...nodes.map(n=>({data:{id:n.id,label:(n.step?n.step+'. ':'')+n.title+'\n'+n.subtitle,kind:n.kind,resourceKind:n.resourceKind||'',disabled:!!n.disabled,error:!!n.error},position:n.position})),...edges.filter(e=>ids.has(e.data.source)&&ids.has(e.data.target))],layout:{name:'preset'},minZoom:.15,maxZoom:2,wheelSensitivity:.2,style:[
|
||||
{selector:'node',style:{shape:'round-rectangle',width:220,height:66,'background-color':'#fff','border-color':'#92a89a','border-width':1.5,label:'data(label)','text-wrap':'wrap','text-max-width':185,'text-valign':'center',color:'#243c32','font-family':'system-ui','font-size':11}},
|
||||
{selector:'node[kind="variable"]',style:{shape:'ellipse','background-color':'#f5eedf'}},
|
||||
{selector:'node[kind="rule"],node[kind="iteration"],node[kind="condition"]',style:{shape:'hexagon','background-color':'#e4eee9',width:240}},
|
||||
{selector:'node[resourceKind="schedule"]',style:{'background-color':'#e5efff','border-color':'#799dd1'}},
|
||||
{selector:'node[resourceKind="escalation"]',style:{'background-color':'#f2e8fb','border-color':'#b18ace'}},
|
||||
{selector:'node[resourceKind="route"]',style:{'background-color':'#fff0d9','border-color':'#c5a46c'}},
|
||||
{selector:'node[?disabled]',style:{opacity:.5}},{selector:'node[?error]',style:{'border-color':'#bd4343','border-width':3}},
|
||||
{selector:'node[kind="guide"]',style:{width:1,height:1,opacity:0,events:'no'}},
|
||||
{selector:'node[kind="lane"]',style:{width:150,height:35,shape:'rectangle','border-width':0,'background-opacity':0,'font-weight':700,'text-max-width':145,events:'no'}},
|
||||
{selector:'edge',style:{width:1.5,'line-color':'#96a99d','target-arrow-color':'#739681','target-arrow-shape':'triangle','curve-style':'bezier'}},
|
||||
{selector:'edge[role="order"]',style:{width:2.5,'line-color':'#428465','curve-style':'straight'}},
|
||||
{selector:'edge[role="dependency"]',style:{'line-style':'dashed',opacity:.5}},
|
||||
{selector:'edge[role="boundary"]',style:{'target-arrow-shape':'none','curve-style':'straight','line-color':'#b9c9be',events:'no'}},
|
||||
{selector:'.related',style:{'border-color':'#286448',opacity:1}},{selector:'node.selected',style:{'border-width':3,'border-color':'#286448'}}
|
||||
]});graph.on('tap','node',e=>{if(ids.has(e.target.id()))this.mapSelect(e.target.id());});graph.on('tap',e=>{if(e.target===graph){this.mapSelected='';graph.elements().removeClass('selected related');}});this.mapFit();if(!ids.has(this.mapSelected))this.mapSelected=nodes.find(n=>n.rule)?.id||nodes[0]?.id||'';if(this.mapSelected)this.mapSelect(this.mapSelected);
|
||||
}
|
||||
};}
|
||||
Reference in new issue
Block a user