File size: 3,271 Bytes
7596726
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
import { buildApiGuideEndpoints } from './api-guide.mjs';

// Builds the header tabs from the generated views plus the local utility panels.
function buildTabs(uiModel) {
  const tabs = uiModel.views.map((view, index) => ({
    id: view.id,
    label: view.label,
    icon: 'fa-table-cells-large',
    active: index === 0,
  }));
  tabs.push({ id: 'data', label: 'Data', icon: 'fa-table' });
  tabs.push({ id: 'api', label: 'REST API', icon: 'fa-book' });
  return tabs;
}

// Creates the shared SolverForge UI shell and the hospital-specific panels.
export function createAppShell({
  root = globalThis,
  sf,
  appElement,
  config,
  uiModel,
  demoId,
  statusBar,
  activeTab,
  actions,
}) {
  const viewRoots = {};
  const header = sf.createHeader({
    logo: '/sf/img/ouroboros.svg',
    title: config.title,
    subtitle: config.subtitle,
    tabs: buildTabs(uiModel),
    actions,
    onTabChange(tabId) {
      shell.setActiveTab(tabId);
      if (typeof actions.onTabChange === 'function') actions.onTabChange(tabId);
    },
  });
  appElement.appendChild(header);
  statusBar.bindHeader(header);
  appElement.appendChild(statusBar.el);

  uiModel.views.forEach((view) => {
    const panel = sf.el('div', { className: 'sf-content', style: { display: 'none' } });
    const rootEl = sf.el('div', { id: `view-${view.id}` });
    panel.appendChild(rootEl);
    viewRoots[view.id] = rootEl;
    appElement.appendChild(panel);
    viewRoots[view.id].panel = panel;
  });

  const dataPanel = sf.el('div', { className: 'sf-content', style: { display: 'none' } });
  const dataRoot = sf.el('div', { id: 'sf-tables' });
  dataPanel.appendChild(dataRoot);
  appElement.appendChild(dataPanel);

  const apiPanel = sf.el('div', { className: 'sf-content', style: { display: 'none' } });
  apiPanel.appendChild(sf.createApiGuide({ endpoints: buildApiGuideEndpoints(demoId, root) }));
  appElement.appendChild(apiPanel);

  appElement.appendChild(sf.createFooter({
    links: [
      { label: 'SolverForge', url: 'https://www.solverforge.org' },
      { label: 'Docs', url: 'https://www.solverforge.org/docs' },
    ],
  }));

  const analysisModal = sf.createModal({ title: 'Score Analysis', width: '700px' });

  const shell = {
    header,
    viewRoots,
    dataRoot,
    dataPanel,
    apiPanel,
    analysisModal,
    setActiveTab(tabId) {
      Object.entries(viewRoots).forEach(([viewId, rootEl]) => {
        rootEl.panel.style.display = viewId === tabId ? '' : 'none';
      });
      dataPanel.style.display = tabId === 'data' ? '' : 'none';
      apiPanel.style.display = tabId === 'api' ? '' : 'none';
    },
    syncLifecycleMarkers({ jobId, snapshotRevision, lifecycleState }) {
      if (jobId) appElement.dataset.jobId = String(jobId);
      else delete appElement.dataset.jobId;

      if (snapshotRevision != null) appElement.dataset.snapshotRevision = String(snapshotRevision);
      else delete appElement.dataset.snapshotRevision;

      if (lifecycleState && lifecycleState !== 'IDLE') appElement.dataset.lifecycleState = lifecycleState;
      else delete appElement.dataset.lifecycleState;
    },
    openAnalysis(body) {
      analysisModal.setBody(body);
      analysisModal.open();
    },
  };

  shell.setActiveTab(activeTab);
  return shell;
}