Files
op-packages/luci-app-hijpass/ts-src/utils/feature/overview/flow-preview.ts
T

429 lines
12 KiB
TypeScript

import { buildConfigurationFlowPreview, type FlowBranch, type FlowDiagram, type FlowStep, type FlowPage } from "./flow-model";
const FLOW_STYLE = `
a.hijpass-flow-step,
a.hijpass-flow-branch__condition {
display: block;
color: inherit;
text-decoration: none;
}
a.hijpass-flow-step:hover,
a.hijpass-flow-branch__condition:hover {
border-color: var(--primary-color, #4b6cb7);
}
a.hijpass-flow-step:focus-visible,
a.hijpass-flow-branch__condition:focus-visible {
outline: 2px solid var(--primary-color, #4b6cb7);
outline-offset: 2px;
}
.hijpass-flow-card h3 {
margin: 0;
}
.hijpass-flow-step[data-tone="warning"] {
color: var(--warning-color, #b26a00);
}
.hijpass-flow-list {
display: grid;
gap: 1rem;
}
.hijpass-flow-card {
--hijpass-flow-node-width: 14rem;
--hijpass-flow-condition-width: 13rem;
box-sizing: border-box;
min-width: 0;
padding: 1rem;
}
.hijpass-flow-scroll {
max-width: 100%;
margin-top: .75rem;
padding: 0 0 .5rem;
overflow-x: auto;
overflow-y: hidden;
overscroll-behavior-inline: contain;
scrollbar-gutter: stable;
}
.hijpass-flow-scroll:focus-visible {
outline: 2px solid var(--primary-color, #4b6cb7);
outline-offset: 2px;
}
.hijpass-flow-tree {
display: flex;
align-items: center;
width: max-content;
min-width: max-content;
}
.hijpass-flow-chain {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: .5rem;
width: max-content;
min-width: max-content;
margin: 0;
padding: 0;
list-style: none;
}
.hijpass-flow-chain > li:not(.hijpass-flow-arrow) {
display: flex;
flex: 0 0 var(--hijpass-flow-node-width);
width: var(--hijpass-flow-node-width);
}
.hijpass-flow-step {
box-sizing: border-box;
width: 100%;
min-width: 0;
padding: .75rem;
border: 1px solid rgba(127, 127, 127, .45);
border-left-width: 4px;
border-radius: 4px;
overflow-wrap: anywhere;
}
.hijpass-flow-step[data-tone="proxy"] {
border-left-color: var(--primary-color, #4b6cb7);
}
.hijpass-flow-step[data-tone="direct"] {
border-left-color: var(--success-color, #2e7d32);
}
.hijpass-flow-step[data-tone="muted"] {
opacity: .7;
}
.hijpass-flow-step[data-tone="warning"] {
border-left-color: currentColor;
}
.hijpass-flow-step__detail,
.hijpass-flow-branch__detail {
display: block;
margin-top: .25rem;
color: var(--text-color-medium, #777);
font-size: .8125rem;
line-height: 1.4;
}
.hijpass-flow-step__detail {
white-space: pre-line;
}
.hijpass-flow-arrow {
display: flex;
flex: 0 0 auto;
align-items: center;
justify-content: center;
color: var(--text-color-medium, #777);
font-size: 1.25rem;
}
.hijpass-flow-branches {
position: relative;
display: grid;
gap: .75rem;
margin: 0;
padding: 0;
list-style: none;
}
.hijpass-flow-branches--parallel {
margin-left: 1.5rem;
}
.hijpass-flow-branches--parallel::before {
position: absolute;
top: 50%;
right: 100%;
width: 1.5rem;
border-top: 2px solid rgba(127, 127, 127, .6);
content: '';
}
.hijpass-flow-branch {
display: grid;
align-items: stretch;
gap: .5rem;
min-height: 4.25rem;
}
.hijpass-flow-branch--parallel {
grid-template-columns: 1.5rem var(--hijpass-flow-condition-width) 1.5rem auto;
}
.hijpass-flow-branch__joint {
position: relative;
display: flex;
align-self: stretch;
align-items: center;
justify-content: flex-end;
color: var(--text-color-medium, #777);
font-size: 1.25rem;
line-height: 1;
}
.hijpass-flow-branch__joint::after {
position: absolute;
top: -.375rem;
bottom: -.375rem;
left: 0;
border-left: 2px solid rgba(127, 127, 127, .6);
content: '';
}
.hijpass-flow-branch--parallel:first-child .hijpass-flow-branch__joint::after {
top: 50%;
}
.hijpass-flow-branch--parallel:last-child .hijpass-flow-branch__joint::after {
bottom: 50%;
}
.hijpass-flow-branch--parallel:only-child .hijpass-flow-branch__joint::after {
display: none;
}
.hijpass-flow-branch__condition {
box-sizing: border-box;
width: var(--hijpass-flow-condition-width);
padding: .75rem;
border: 1px dashed rgba(127, 127, 127, .55);
border-radius: 4px;
overflow-wrap: anywhere;
}
.hijpass-flow-branch .hijpass-flow-chain {
margin: 0;
}
.hijpass-flow-ordered {
display: grid;
grid-template-columns: auto auto auto;
grid-template-rows: auto auto auto;
column-gap: .5rem;
width: max-content;
min-width: max-content;
align-items: start;
}
.hijpass-flow-ordered__prefix {
grid-column: 1;
grid-row: 2;
align-self: center;
}
.hijpass-flow-ordered__entry {
grid-column: 2;
grid-row: 2;
align-self: center;
width: auto;
}
.hijpass-flow-ordered__description {
display: flex;
grid-column: 3;
grid-row: 1;
align-items: center;
height: 1.5rem;
margin: 0 0 .25rem;
color: var(--text-color-medium, #777);
font-size: .8125rem;
line-height: 1.25;
white-space: nowrap;
}
.hijpass-flow-ordered__first,
.hijpass-flow-ordered__rest {
grid-column: 3;
}
.hijpass-flow-ordered__first {
grid-row: 2;
align-self: stretch;
}
.hijpass-flow-ordered__rest {
grid-row: 3;
}
.hijpass-flow-branches--ordered {
gap: 0;
}
.hijpass-flow-branch--ordered {
grid-template-columns: var(--hijpass-flow-condition-width) 1.5rem auto;
}
.hijpass-flow-branch--ordered .hijpass-flow-branch__condition,
.hijpass-flow-branch--ordered .hijpass-flow-step {
display: flex;
min-height: 4.25rem;
flex-direction: column;
justify-content: center;
}
.hijpass-flow-branch--ordered .hijpass-flow-chain {
align-items: stretch;
}
.hijpass-flow-order-arrow {
display: flex;
width: var(--hijpass-flow-condition-width);
height: 1.25rem;
align-items: center;
justify-content: center;
color: var(--text-color-medium, #777);
font-size: 1rem;
line-height: 1;
}
@media (max-width: 900px) {
.hijpass-flow-card {
--hijpass-flow-node-width: 13rem;
--hijpass-flow-condition-width: 12rem;
}
}
`;
function ensureFlowPreviewStyles() {
if (document.getElementById('hijpass-flow-preview-styles')) return;
const style = document.createElement('style');
style.id = 'hijpass-flow-preview-styles';
style.textContent = FLOW_STYLE;
document.head.appendChild(style);
}
function pageUrl(page: FlowPage): string {
return L.url(page === 'dhcp' ? 'admin/network/dhcp' : 'admin/services/hijpass/' + page);
}
function renderStep(step: FlowStep) {
return E(step.page ? 'a' : 'div', {
...(step.page ? { href: pageUrl(step.page) } : {}),
'class': 'hijpass-flow-step',
'data-tone': step.tone || 'normal',
}, [
E('strong', {}, step.title),
step.detail ? E('span', { 'class': 'hijpass-flow-step__detail' }, step.detail) : '',
]);
}
function renderChain(steps: FlowStep[]) {
const children: HTMLElement[] = [];
steps.forEach((step, index) => {
children.push(E('li', {}, [renderStep(step)]) as HTMLElement);
if (index < steps.length - 1) {
children.push(E('li', { 'class': 'hijpass-flow-arrow', 'aria-hidden': 'true' }, '→') as HTMLElement);
}
});
return E('ol', { 'class': 'hijpass-flow-chain' }, children);
}
function renderParallelBranch(branch: FlowBranch) {
return E('li', { 'class': 'hijpass-flow-branch hijpass-flow-branch--parallel' }, [
E('span', { 'class': 'hijpass-flow-branch__joint', 'aria-hidden': 'true' }, '→'),
E(branch.page ? 'a' : 'div', { 'class': 'hijpass-flow-branch__condition', ...(branch.page ? { href: pageUrl(branch.page) } : {}) }, [
E('strong', {}, branch.condition),
branch.detail ? E('span', { 'class': 'hijpass-flow-branch__detail' }, branch.detail) : '',
]),
E('span', { 'class': 'hijpass-flow-arrow', 'aria-hidden': 'true' }, '→'),
renderChain(branch.steps),
]);
}
function renderOrderedBranch(branch: FlowBranch) {
return E('li', { 'class': 'hijpass-flow-branch hijpass-flow-branch--ordered' }, [
E(branch.page ? 'a' : 'div', { 'class': 'hijpass-flow-branch__condition', ...(branch.page ? { href: pageUrl(branch.page) } : {}) }, [
E('strong', {}, branch.condition),
branch.detail ? E('span', { 'class': 'hijpass-flow-branch__detail' }, branch.detail) : '',
]),
E('span', { 'class': 'hijpass-flow-arrow', 'aria-hidden': 'true' }, '→'),
renderChain(branch.steps),
]);
}
function renderOrderedTree(steps: FlowStep[], branches: FlowBranch[], description?: string) {
const remaining: HTMLElement[] = [];
branches.slice(1).forEach(branch => {
remaining.push(E('li', {
'class': 'hijpass-flow-order-arrow',
'aria-hidden': 'true',
}, '↓') as HTMLElement);
remaining.push(renderOrderedBranch(branch) as HTMLElement);
});
return E('div', {
'class': 'hijpass-flow-tree hijpass-flow-ordered',
'data-branch-mode': 'ordered',
}, [
E('p', { 'class': 'hijpass-flow-ordered__description' }, description || ''),
E('div', { 'class': 'hijpass-flow-ordered__prefix' }, renderChain(steps)),
E('span', { 'class': 'hijpass-flow-arrow hijpass-flow-ordered__entry', 'aria-hidden': 'true' }, '→'),
E('ol', {
'class': 'hijpass-flow-branches hijpass-flow-branches--ordered hijpass-flow-ordered__first',
}, [renderOrderedBranch(branches[0])]),
remaining.length > 0 ? E('ol', {
'class': 'hijpass-flow-branches hijpass-flow-branches--ordered hijpass-flow-ordered__rest',
}, remaining) : '',
]);
}
function renderBranches(diagram: FlowDiagram) {
if (!diagram.branches?.length) return '';
return E('ul', {
'class': 'hijpass-flow-branches hijpass-flow-branches--parallel',
}, diagram.branches.map(renderParallelBranch));
}
function renderTree(diagram: FlowDiagram) {
const tree = diagram.branchMode === 'ordered' && diagram.branches?.length
? renderOrderedTree(diagram.steps, diagram.branches, diagram.branchDescription)
: E('div', {
'class': 'hijpass-flow-tree',
'data-branch-mode': diagram.branchMode || 'parallel',
}, [
renderChain(diagram.steps),
renderBranches(diagram),
]);
return E('div', {
'class': 'hijpass-flow-scroll',
'role': 'region',
'aria-label': diagram.title,
'tabindex': '0',
}, [tree]);
}
const FlowPreviewUtils = {
createConfigurationFlowPreview: function () {
ensureFlowPreviewStyles();
const preview = buildConfigurationFlowPreview();
return E('div', { 'class': 'hijpass-flow-preview' }, [
E('div', { 'class': 'cbi-section-descr' },
_('Generated from saved settings. Branches show configured decisions, not a live packet trace.')),
E('div', { 'class': 'hijpass-flow-list' }, preview.diagrams.map(diagram =>
E('section', {
'class': 'hijpass-flow-card',
'aria-labelledby': 'hijpass-flow-' + diagram.id,
}, [
E('h3', { 'id': 'hijpass-flow-' + diagram.id }, diagram.title),
renderTree(diagram),
])))
]);
},
};
export { FlowPreviewUtils };