tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

overview.html (51596B)


      1 <!DOCTYPE html>
      2 <html lang="en">
      3  <head>
      4    <meta charset="UTF-8" />
      5    <meta name="viewport" content="width=device-width, initial-scale=1" />
      6    <title>Overview - Web Platform Test</title>
      7    <link rel="stylesheet" href="css/bulma-0.7.5/bulma.min.css" />
      8    <link rel="stylesheet" href="css/fontawesome-5.7.2.min.css" />
      9    <script src="lib/utils.js"></script>
     10    <script src="lib/wave-service.js"></script>
     11    <script src="lib/ui.js"></script>
     12    <style>
     13      .site-logo {
     14        max-width: 300px;
     15        margin: 0 0 30px -15px;
     16      }
     17 
     18      .disabled-row {
     19        color: gray;
     20        background: lightgray;
     21      }
     22    </style>
     23  </head>
     24  <body>
     25    <script>
     26      window.onload = () => {
     27        const query = utils.parseQuery(location.search);
     28        if (query.token) {
     29          location.href = WEB_ROOT + "results.html" + location.search;
     30        }
     31        resultsUi.render();
     32        resultsUi.loadData();
     33      };
     34      var sortDetail = {};
     35      const defaultSortDetail = { sortColumn: "dateStarted", ascending: true };
     36      sortDetail["recentSessions"] = defaultSortDetail;
     37      sortDetail["pinnedSessions"] = defaultSortDetail;
     38      sortDetail["publicSessions"] = defaultSortDetail;
     39 
     40      const resultsUi = {
     41        state: {
     42          comparison: [],
     43          recentSessions: null,
     44          importResultsEnabled: false,
     45          filterLabels: []
     46        },
     47        loadData() {
     48          const pinnedSessions = WaveService.getPinnedSessions().filter(
     49            token => !!token
     50          );
     51          const recentSessions = WaveService.getRecentSessions().filter(
     52            token => !!token
     53          );
     54 
     55          pinnedSessions.forEach(token => {
     56            const index = recentSessions.indexOf(token);
     57            if (index !== -1) recentSessions.splice(index, 1);
     58          });
     59          WaveService.setRecentSessions(recentSessions);
     60 
     61          let allSessions = [];
     62          allSessions = allSessions.concat(pinnedSessions);
     63          allSessions = allSessions.concat(recentSessions);
     64 
     65          WaveService.readPublicSessions(publicSessions => {
     66            publicSessions.forEach(token => {
     67              const index = recentSessions.indexOf(token);
     68              if (index !== -1) recentSessions.splice(index, 1);
     69            });
     70            WaveService.setRecentSessions(recentSessions);
     71            allSessions = allSessions.concat(publicSessions);
     72            WaveService.readMultipleSessions(allSessions, configurations =>
     73              WaveService.readMultipleSessionStatuses(allSessions, statuses => {
     74                configurations.forEach(configuration => {
     75                  const status = statuses.find(
     76                    status => status.token === configuration.token
     77                  );
     78                  configuration.dateStarted = status.dateStarted;
     79                  configuration.dateFinished = status.dateFinished;
     80                  configuration.status = status.status;
     81                });
     82 
     83                configurations = configurations.filter(
     84                  configuration => !!configuration
     85                );
     86                allSessions
     87                  .filter(
     88                    token =>
     89                      !configurations.some(
     90                        configuration => configuration.token === token
     91                      )
     92                  )
     93                  .forEach(token => {
     94                    WaveService.removePinnedSession(token);
     95                    WaveService.removeRecentSession(token);
     96                  });
     97                resultsUi.state.publicSessions = publicSessions;
     98                resultsUi.state.pinnedSessions = WaveService.getPinnedSessions();
     99                resultsUi.state.recentSessions = WaveService.getRecentSessions();
    100 
    101                const sessions = {};
    102                configurations.forEach(
    103                  configuration =>
    104                    (sessions[configuration.token] = configuration)
    105                );
    106                resultsUi.state.sessions = sessions;
    107 
    108                const referenceTokens = [];
    109                const loadedSessionsTokens = Object.keys(sessions);
    110                configurations.forEach(configuration =>
    111                  configuration.referenceTokens
    112                    .filter(token => loadedSessionsTokens.indexOf(token) === -1)
    113                    .forEach(token => referenceTokens.push(token))
    114                );
    115                WaveService.readMultipleSessions(
    116                  referenceTokens,
    117                  configurations => {
    118                    const { sessions } = resultsUi.state;
    119                    configurations.forEach(
    120                      configuration =>
    121                        (sessions[configuration.token] = configuration)
    122                    );
    123                    resultsUi.renderPublicSessions();
    124                    resultsUi.renderPinnedSessions();
    125                    resultsUi.renderRecentSessions();
    126                  }
    127                );
    128              })
    129            );
    130          });
    131          WaveService.readStatus(function(config) {
    132            resultsUi.state.importResultsEnabled = config.importResultsEnabled;
    133            resultsUi.state.reportsEnabled = config.reportsEnabled;
    134            resultsUi.renderManageSessions();
    135          });
    136        },
    137        findSession(fragment, callback) {
    138          if (!fragment || fragment.length < 8) return;
    139          WaveService.findToken(
    140            fragment,
    141            token => {
    142              WaveService.readSession(token, session => {
    143                WaveService.readSessionStatus(token, status => {
    144                  session.status = status.status;
    145                  session.dateStarted = status.dateStarted;
    146                  session.dateFinished = status.dateFinished;
    147                  callback(session);
    148                });
    149              });
    150            },
    151            () => callback(null)
    152          );
    153        },
    154        addSession(session) {
    155          const token = session.token;
    156          if (resultsUi.state.sessions[token]) return;
    157          resultsUi.state.sessions[token] = session;
    158          resultsUi.pinSession(token);
    159        },
    160        removeSession(token) {
    161          delete resultsUi.state.sessions[token];
    162          WaveService.removeRecentSession(token);
    163          WaveService.removePinnedSession(token);
    164          resultsUi.updateSessionState();
    165        },
    166        showAddSessionError() {
    167          const errorBox = UI.getElement("find-error");
    168          errorBox.setAttribute("style", "display: block");
    169        },
    170        hideAddSessionError() {
    171          const errorBox = UI.getElement("find-error");
    172          errorBox.setAttribute("style", "display: none");
    173        },
    174        pinSession(token) {
    175          WaveService.addPinnedSession(token);
    176          WaveService.removeRecentSession(token);
    177          resultsUi.updateSessionState();
    178        },
    179        unpinSession(token) {
    180          WaveService.removePinnedSession(token);
    181          WaveService.addRecentSession(token);
    182          resultsUi.updateSessionState();
    183        },
    184        updateSessionState() {
    185          resultsUi.state.pinnedSessions = WaveService.getPinnedSessions();
    186          resultsUi.state.recentSessions = WaveService.getRecentSessions();
    187          resultsUi.renderPinnedSessions();
    188          resultsUi.renderRecentSessions();
    189        },
    190        openSessionResult(token) {
    191 				location.href = `${WEB_ROOT}results.html?token=${token}`;
    192        },
    193        sortSessions(tableType, column) {
    194          if (tableType in sortDetail) {
    195            if (sortDetail[tableType].sortColumn == column) {
    196              sortDetail[tableType].ascending = !sortDetail[tableType]
    197                .ascending;
    198            } else {
    199              sortDetail[tableType].sortColumn = column;
    200              sortDetail[tableType].ascending = true;
    201            }
    202            switch (tableType) {
    203              case "recentSessions":
    204                resultsUi.renderRecentSessions();
    205                break;
    206              case "pinnedSessions":
    207                resultsUi.renderPinnedSessions();
    208                break;
    209              case "publicSessions":
    210                resultsUi.renderPublicSessions();
    211                break;
    212            }
    213          }
    214        },
    215        sortSessionsByColumn(sessions, recentSessions, column, ascending) {
    216          var resultArray = recentSessions
    217            .map(token => sessions[token])
    218            .sort(function(sessionA, sessionB) {
    219              let columnA = sessionA[column];
    220              if (column === "browser")
    221                columnA = sessionA[column].name + sessionA[column].version;
    222              if (column === "dateStarted" && !columnA) {
    223                columnA = Date.now();
    224              }
    225              let columnB = sessionB[column];
    226              if (column === "browser")
    227                columnB = sessionB[column].name + sessionA[column].version;
    228              if (column === "dateStarted" && !columnB) {
    229                columnB = Date.now();
    230              }
    231              if (columnA < columnB) {
    232                return -1;
    233              }
    234              if (columnA > columnB) {
    235                return 1;
    236              }
    237              return 0;
    238            });
    239          if (ascending) {
    240            resultArray.reverse();
    241          }
    242          return resultArray;
    243        },
    244        compareSessions(reftokens) {
    245          if (!resultsUi.isComparisonValid()) return;
    246          const tokens = resultsUi.state.comparison;
    247          if (!tokens || tokens.length === 0) return;
    248          const refQuery = reftokens ? `&reftokens=${reftokens}` : "";
    249 				location.href = `${WEB_ROOT}comparison.html?tokens=${tokens.join(
    250            ","
    251          )}${refQuery}`;
    252        },
    253        isComparisonValid() {
    254          const { comparison, sessions } = resultsUi.state;
    255          if (!comparison) return false;
    256          if (comparison.length <= 1) return false;
    257          const comparingSessions = comparison.map(token => sessions[token]);
    258          const referenceTokens = comparingSessions[0].referenceTokens;
    259          for (let comparingSession of comparingSessions) {
    260            const comparingReferenceTokens = comparingSession.referenceTokens;
    261            if (referenceTokens.length !== comparingReferenceTokens.length)
    262              return false;
    263            for (let token of comparingReferenceTokens) {
    264              if (referenceTokens.indexOf(token) === -1) return false;
    265            }
    266          }
    267          return true;
    268        },
    269        isSessionValidForComparison(session) {
    270          if (!session) return false;
    271          if (session.status !== "completed" && session.status !== "aborted")
    272            return false;
    273          const sessionRefTokens = session.reference_tokens;
    274          const comparisonSession =
    275            resultsUi.state.sessions[resultsUi.state.comparison[0]];
    276          if (!comparisonSession) return true;
    277          const comparisonRefTokens = comparisonSession.reference_tokens;
    278          if (!comparisonRefTokens) return true;
    279          if (sessionRefTokens.length !== comparisonRefTokens.length)
    280            return false;
    281          if (
    282            sessionRefTokens.some(
    283              token => comparisonRefTokens.indexOf(token) === -1
    284            )
    285          )
    286            return false;
    287          return true;
    288        },
    289        isSessionSelectedForComparison(session) {
    290          return resultsUi.state.comparison.indexOf(session.token) !== -1;
    291        },
    292        isSessionDisabled(session) {
    293          return (
    294            resultsUi.state.comparison.length > 0 &&
    295            !resultsUi.isSessionValidForComparison(session)
    296          );
    297        },
    298        addSessionToComparison(token) {
    299          if (resultsUi.state.comparison.indexOf(token) !== -1) return;
    300          resultsUi.state.comparison.push(token);
    301          resultsUi.updateCompareButton();
    302          resultsUi.renderSessions();
    303        },
    304        removeSessionFromComparison(token) {
    305          const index = resultsUi.state.comparison.indexOf(token);
    306          if (index === -1) return;
    307          resultsUi.state.comparison.splice(index, 1);
    308          resultsUi.updateCompareButton();
    309          resultsUi.renderSessions();
    310        },
    311        handleAddSession() {
    312          const tokenFragmentInput = UI.getElement("token-fragment");
    313          const fragment = tokenFragmentInput.value;
    314          resultsUi.findSession(fragment, session => {
    315            if (!session) {
    316              resultsUi.showAddSessionError();
    317              return;
    318            }
    319            tokenFragmentInput.value = "";
    320            resultsUi.hideAddSessionError();
    321            resultsUi.addSession(session);
    322          });
    323        },
    324        handleImportSession() {
    325          resultsUi.state.importError = null;
    326          resultsUi.state.importInProgress = true;
    327          resultsUi.renderManageSessions();
    328          const { importSessionFile: file } = resultsUi.state;
    329          const reader = new FileReader();
    330          reader.readAsArrayBuffer(file);
    331          reader.onload = () => {
    332            const data = reader.result;
    333            WaveService.importResults(
    334              data,
    335              function(token) {
    336                location.href = WEB_ROOT + "results.html?token=" + token;
    337              },
    338              function(error) {
    339                resultsUi.state.importError = error;
    340                resultsUi.state.importInProgress = false;
    341                resultsUi.renderManageSessions();
    342              }
    343            );
    344          };
    345        },
    346        handleImportSessionSelection() {
    347          const file = UI.getElement("import-session-file").files[0];
    348          resultsUi.state.importSessionFile = file;
    349          resultsUi.renderManageSessions();
    350        },
    351        addFilterLabel() {
    352          const label = UI.getElement("filter-label-input").value;
    353          if (!label) return;
    354          const { filterLabels } = resultsUi.state;
    355          if (filterLabels.indexOf(label) !== -1) return;
    356          filterLabels.push(label);
    357          resultsUi.renderSessions();
    358          UI.getElement("filter-label-input").focus();
    359        },
    360        removeFilterLabel(index) {
    361          resultsUi.state.filterLabels.splice(index, 1);
    362          resultsUi.renderSessions();
    363        },
    364        showAddFilterLabel() {
    365          resultsUi.state.addFilterLabelVisible = true;
    366          resultsUi.renderSessions();
    367          UI.getElement("filter-label-input").focus();
    368        },
    369        hideAddFilterLabel() {
    370          resultsUi.state.addFilterLabelVisible = false;
    371          resultsUi.renderSessions();
    372        },
    373        render() {
    374          const { getRoot, createElement, getElement } = UI;
    375          const resultsView = UI.createElement({
    376            className: "section",
    377            children: [
    378              {
    379                className: "container",
    380                style: "margin-bottom: 2em",
    381                children: [
    382                  {
    383                    element: "img",
    384                    src: "res/wavelogo_2016.jpg",
    385                    className: "site-logo"
    386                  },
    387                  { text: "Results Overview", className: "title" }
    388                ]
    389              },
    390              {
    391                id: "manage-sessions",
    392                className: "container",
    393                style: "margin-bottom: 2em"
    394              },
    395              { id: "sessions", className: "container" }
    396            ]
    397          });
    398 
    399          const root = UI.getRoot();
    400          root.innerHTML = "";
    401          root.appendChild(resultsView);
    402 
    403          resultsUi.renderManageSessions();
    404          resultsUi.renderSessions();
    405        },
    406        renderManageSessions() {
    407          const manageSessionsView = UI.getElement("manage-sessions");
    408          manageSessionsView.innerHTML = "";
    409          const heading = { text: "Manage Sessions", className: "title is-4" };
    410          const addCompareSessions = {
    411            className: "columns",
    412            children: [
    413              {
    414                className: "column",
    415                children: [
    416                  { text: "Add Sessions", className: "title is-5" },
    417                  {
    418                    element: "article",
    419                    className: "message is-danger",
    420                    id: "find-error",
    421                    children: [
    422                      {
    423                        text:
    424                          "Could not find any sessions! Try adding more characters of the token.",
    425                        className: "message-body"
    426                      }
    427                    ],
    428                    style: "display: none"
    429                  },
    430                  {
    431                    className: "field",
    432                    children: [
    433                      {
    434                        className: "label has-text-weight-normal",
    435                        text: "Session token:"
    436                      },
    437                      {
    438                        className: "field-body",
    439                        children: {
    440                          className: "field",
    441                          children: {
    442                            className: "control",
    443                            children: {
    444                              style: "display: flex; margin-bottom: 10px;",
    445                              children: [
    446                                {
    447                                  element: "input",
    448                                  inputType: "text",
    449                                  className: "input is-family-monospace",
    450                                  id: "token-fragment",
    451                                  placeholder:
    452                                    "First 8 characters or more of session token",
    453                                  onKeyDown: event =>
    454                                    event.key === "Enter"
    455                                      ? resultsUi.handleAddSession()
    456                                      : null
    457                                }
    458                              ]
    459                            }
    460                          }
    461                        }
    462                      },
    463                      {
    464                        className: "field is-grouped is-grouped-right",
    465                        children: {
    466                          className: "control",
    467                          children: {
    468                            className: "button is-dark is-outlined",
    469                            children: [
    470                              {
    471                                element: "span",
    472                                className: "icon",
    473                                children: [
    474                                  {
    475                                    element: "i",
    476                                    className: "fas fa-plus"
    477                                  }
    478                                ]
    479                              },
    480                              { text: "Add Session", element: "span" }
    481                            ],
    482                            onclick: resultsUi.handleAddSession
    483                          }
    484                        }
    485                      }
    486                    ]
    487                  }
    488                ]
    489              },
    490              {
    491                className: "column",
    492                children: [
    493                  { text: "Compare Sessions", className: "title is-5" },
    494                  {
    495                    element: "label",
    496                    text:
    497                      "Compare sessions by selecting them in the list below. " +
    498                      "Only sessions with the same set of reference sessions can be compared. " +
    499                      "Sessions have to be finished."
    500                  },
    501                  {
    502                    style: "text-align: right",
    503                    children: [
    504                      {
    505                        className: "button is-dark is-outlined",
    506                        disabled: true,
    507                        id: "compare-button",
    508                        children: [
    509                          {
    510                            element: "span",
    511                            className: "icon",
    512                            children: [
    513                              {
    514                                element: "i",
    515                                className: "fas fa-balance-scale"
    516                              }
    517                            ]
    518                          },
    519                          { text: "Compare Selected", element: "span" }
    520                        ],
    521                        onClick: () => resultsUi.compareSessions()
    522                      }
    523                    ]
    524                  }
    525                ]
    526              }
    527            ]
    528          };
    529          const {
    530            importSessionFile,
    531            importError,
    532            importInProgress
    533          } = resultsUi.state;
    534          const importSessions = {
    535            className: "columns",
    536            style: "margin-bottom: 2em",
    537            children: [
    538              {
    539                className: "column is-half",
    540                children: [
    541                  { text: "Import Sessions", className: "title is-5" },
    542                  {
    543                    element: "article",
    544                    className: "message is-danger",
    545                    children: [
    546                      {
    547                        className: "message-body",
    548                        text: "Could not import session: " + importError
    549                      }
    550                    ],
    551                    style: importError ? "" : "display: none"
    552                  },
    553                  {
    554                    className: "field file has-name",
    555                    children: [
    556                      {
    557                        element: "label",
    558                        className: "file-label",
    559                        style: "width: 100%",
    560                        children: [
    561                          {
    562                            element: "input",
    563                            className: "file-input",
    564                            type: "file",
    565                            accept: ".zip",
    566                            id: "import-session-file",
    567                            onChange: resultsUi.handleImportSessionSelection
    568                          },
    569                          {
    570                            element: "span",
    571                            className: "file-cta",
    572                            children: [
    573                              {
    574                                element: "span",
    575                                className: "file-icon",
    576                                children: [
    577                                  {
    578                                    element: "i",
    579                                    className: "fas fa-upload"
    580                                  }
    581                                ]
    582                              },
    583                              {
    584                                element: "span",
    585                                className: "file-label",
    586                                text: "Choose ZIP file"
    587                              }
    588                            ]
    589                          },
    590                          {
    591                            element: "span",
    592                            className: "file-name",
    593                            style: "width: 100%; max-width: unset",
    594                            text: importSessionFile
    595                              ? importSessionFile.name
    596                              : ""
    597                          }
    598                        ]
    599                      }
    600                    ]
    601                  },
    602                  {
    603                    className: "field is-grouped is-grouped-right",
    604                    children: {
    605                      className: "control",
    606                      children: {
    607                        className: "button is-dark is-outlined",
    608                        disabled: !importSessionFile,
    609                        children: [
    610                          {
    611                            element: "span",
    612                            className: "icon",
    613                            children: [
    614                              {
    615                                element: "i",
    616                                className: importInProgress
    617                                  ? "fas fa-spinner fa-pulse"
    618                                  : "fas fa-plus"
    619                              }
    620                            ]
    621                          },
    622                          { text: "Import Session", element: "span" }
    623                        ],
    624                        onclick: resultsUi.handleImportSession
    625                      }
    626                    }
    627                  }
    628                ]
    629              },
    630              {
    631                className: "column",
    632                children: []
    633              }
    634            ]
    635          };
    636          const { importResultsEnabled } = resultsUi.state;
    637          manageSessionsView.appendChild(UI.createElement(heading));
    638          manageSessionsView.appendChild(UI.createElement(addCompareSessions));
    639          if (!importResultsEnabled) return;
    640          manageSessionsView.appendChild(UI.createElement(importSessions));
    641        },
    642        renderSessions() {
    643          const sessionsView = UI.getElement("sessions");
    644          sessionsView.innerHTML = "";
    645          sessionsView.appendChild(
    646            UI.createElement({ text: "Sessions", className: "title is-4" })
    647          );
    648 
    649          const sessionFilters = resultsUi.createSessionFilters();
    650          sessionsView.appendChild(sessionFilters);
    651 
    652          sessionsView.appendChild(UI.createElement({ id: "public-sessions" }));
    653          sessionsView.appendChild(UI.createElement({ id: "pinned-sessions" }));
    654          sessionsView.appendChild(UI.createElement({ id: "recent-sessions" }));
    655          sessionsView.appendChild(UI.createElement({ id: "session-status" }));
    656          resultsUi.renderPublicSessions();
    657          resultsUi.renderPinnedSessions();
    658          resultsUi.renderRecentSessions();
    659        },
    660        renderPublicSessions() {
    661          resultsUi.renderSessionStatus();
    662          const { sessions, publicSessions, filterLabels } = resultsUi.state;
    663 
    664          UI.saveScrollPosition("public-sessions-overflow");
    665 
    666          const publicSessionsView = UI.getElement("public-sessions");
    667          publicSessionsView.innerHTML = "";
    668 
    669          if (!publicSessions || publicSessions.length === 0) return;
    670          const sortedPublicSessions = resultsUi.sortSessionsByColumn(
    671            sessions,
    672            publicSessions,
    673            sortDetail["publicSessions"].sortColumn,
    674            sortDetail["publicSessions"].ascending
    675          );
    676 
    677          const filteredPublicSessions = sortedPublicSessions.filter(
    678            session =>
    679              filterLabels.length === 0 ||
    680              filterLabels.reduce(
    681                (match, label) =>
    682                  match &&
    683                  session.labels
    684                    .map(label => label.toLowerCase())
    685                    .indexOf(label.toLowerCase()) !== -1,
    686                true
    687              )
    688          );
    689 
    690          if (filteredPublicSessions.length === 0) return;
    691 
    692          publicSessionsView.appendChild(
    693            UI.createElement({
    694              text: "Reference Browsers",
    695              className: "title is-5"
    696            })
    697          );
    698 
    699          const sessionsTable = UI.createElement({
    700            style: "overflow-x: auto",
    701            id: "public-sessions-overflow",
    702            children: resultsUi.createSessionsTable(
    703              "publicSessions",
    704              filteredPublicSessions,
    705              { static: true }
    706            )
    707          });
    708          publicSessionsView.appendChild(sessionsTable);
    709 
    710          publicSessionsView.appendChild(
    711            UI.createElement({ style: "content: ''; margin-bottom: 40px" })
    712          );
    713 
    714          UI.loadScrollPosition("public-sessions-overflow")
    715        },
    716        renderPinnedSessions() {
    717          resultsUi.renderSessionStatus();
    718          const { sessions, pinnedSessions, filterLabels } = resultsUi.state;
    719 
    720          UI.saveScrollPosition("pinned-sessions-overflow");
    721          const pinnedSessionsView = UI.getElement("pinned-sessions");
    722          pinnedSessionsView.innerHTML = "";
    723          if (!pinnedSessions || pinnedSessions.length === 0) return;
    724          const sortedPinnedSessions = resultsUi.sortSessionsByColumn(
    725            sessions,
    726            pinnedSessions,
    727            sortDetail["pinnedSessions"].sortColumn,
    728            sortDetail["pinnedSessions"].ascending
    729          );
    730          const filteredPinnedSessions = sortedPinnedSessions.filter(
    731            session =>
    732              filterLabels.length === 0 ||
    733              filterLabels.reduce(
    734                (match, label) =>
    735                  match &&
    736                  session.labels
    737                    .map(label => label.toLowerCase())
    738                    .indexOf(label.toLowerCase()) !== -1,
    739                true
    740              )
    741          );
    742 
    743          if (filteredPinnedSessions.length === 0) return;
    744 
    745          pinnedSessionsView.appendChild(
    746            UI.createElement({ text: "Pinned", className: "title is-5" })
    747          );
    748 
    749          const sessionsTable = UI.createElement({
    750            style: "overflow-x: auto",
    751            id: "pinned-sessions-overflow",
    752            children: resultsUi.createSessionsTable(
    753              "pinnedSessions",
    754              filteredPinnedSessions,
    755              { pinned: true }
    756            )
    757          });
    758          pinnedSessionsView.appendChild(sessionsTable);
    759 
    760          pinnedSessionsView.appendChild(
    761            UI.createElement({ style: "content: ''; margin-bottom: 40px" })
    762          );
    763          UI.loadScrollPosition("pinned-sessions-overflow");
    764        },
    765        renderRecentSessions() {
    766          resultsUi.renderSessionStatus();
    767          const {
    768            sessions,
    769            recentSessions,
    770            pinnedSessions,
    771            filterLabels
    772          } = resultsUi.state;
    773          UI.saveScrollPosition("recent-sessions-overflow");
    774          const recentSessionsView = UI.getElement("recent-sessions");
    775          recentSessionsView.innerHTML = "";
    776          if (!recentSessions || recentSessions.length === 0) return;
    777 
    778          const sortedRecentSessions = resultsUi.sortSessionsByColumn(
    779            sessions,
    780            recentSessions,
    781            sortDetail["recentSessions"].sortColumn,
    782            sortDetail["recentSessions"].ascending
    783          );
    784          const filteredRecentSessions = sortedRecentSessions.filter(
    785            session =>
    786              filterLabels.length === 0 ||
    787              filterLabels.reduce(
    788                (match, label) =>
    789                  match &&
    790                  session.labels
    791                    .map(label => label.toLowerCase())
    792                    .indexOf(label.toLowerCase()) !== -1,
    793                true
    794              )
    795          );
    796 
    797          if (filteredRecentSessions.length === 0) return;
    798 
    799          recentSessionsView.appendChild(
    800            UI.createElement({ text: "Recent", className: "title is-5" })
    801          );
    802 
    803          const sessionsTable = UI.createElement({
    804            style: "overflow-x: auto",
    805            id: "recent-sessions-overflow",
    806            children: resultsUi.createSessionsTable(
    807              "recentSessions",
    808              filteredRecentSessions,
    809              { pinned: false }
    810            )
    811          });
    812          recentSessionsView.appendChild(sessionsTable);
    813 
    814          recentSessionsView.appendChild(
    815            UI.createElement({ style: "content: ''; margin-bottom: 40px" })
    816          );
    817          UI.loadScrollPosition("recent-sessions-overflow");
    818        },
    819        renderSessionStatus() {
    820          const {
    821            recentSessions,
    822            pinnedSessions,
    823            publicSessions
    824          } = resultsUi.state;
    825          const sessionStatusView = UI.getElement("session-status");
    826          sessionStatusView.innerHTML = "";
    827          if (!recentSessions && !pinnedSessions && !publicSessions) {
    828            sessionStatusView.appendChild(
    829              UI.createElement({
    830                className: "level",
    831                children: {
    832                  element: "span",
    833                  className: "level-item",
    834                  children: [
    835                    {
    836                      element: "i",
    837                      className: "fas fa-spinner fa-pulse"
    838                    },
    839                    {
    840                      style: "margin-left: 0.4em;",
    841                      text: "Loading sessions ..."
    842                    }
    843                  ]
    844                }
    845              })
    846            );
    847            return;
    848          } else if (
    849            (!recentSessions || recentSessions.length === 0) &&
    850            (!pinnedSessions || pinnedSessions.length === 0) &&
    851            (!publicSessions || publicSessions.length === 0)
    852          ) {
    853            sessionStatusView.appendChild(
    854              UI.createElement({
    855                className: "level",
    856                children: {
    857                  element: "span",
    858                  className: "level-item",
    859                  text: "No sessions available."
    860                }
    861              })
    862            );
    863            return;
    864          }
    865        },
    866        createSessionFilters() {
    867          const { filterLabels, addFilterLabelVisible } = resultsUi.state;
    868 
    869          const filters = UI.createElement({
    870            className: "field is-horizontal",
    871            style: "margin-bottom: 2em",
    872            children: [
    873              {
    874                className: "field-label",
    875                style: "flex: unset",
    876                children: {
    877                  className: "label has-text-weight-normal",
    878                  text: "Filter by labels:"
    879                }
    880              },
    881              {
    882                className: "field-body",
    883                children: {
    884                  className: "control",
    885                  children: {
    886                    className: "field is-grouped is-grouped-multiline",
    887                    children: filterLabels
    888                      .map((label, index) => ({
    889                        className: "control",
    890                        children: {
    891                          className: "tags has-addons",
    892                          children: [
    893                            {
    894                              element: "span",
    895                              className: "tag is-info",
    896                              text: label
    897                            },
    898                            {
    899                              element: "a",
    900                              className: "tag is-delete",
    901                              onClick: () => resultsUi.removeFilterLabel(index)
    902                            }
    903                          ]
    904                        }
    905                      }))
    906                      .concat(
    907                        addFilterLabelVisible
    908                          ? [
    909                              {
    910                                className: "control field is-grouped",
    911                                children: [
    912                                  {
    913                                    element: "input",
    914                                    className: "input is-small control",
    915                                    style: "width: 10rem",
    916                                    id: "filter-label-input",
    917                                    type: "text",
    918                                    onKeyUp: event =>
    919                                      event.keyCode === 13
    920                                        ? resultsUi.addFilterLabel()
    921                                        : null
    922                                  },
    923                                  {
    924                                    className:
    925                                      "button is-dark is-outlined is-small is-rounded control",
    926                                    text: "save",
    927                                    onClick: resultsUi.addFilterLabel
    928                                  },
    929                                  {
    930                                    className:
    931                                      "button is-dark is-outlined is-small is-rounded control",
    932                                    text: "cancel",
    933                                    onClick: resultsUi.hideAddFilterLabel
    934                                  }
    935                                ]
    936                              }
    937                            ]
    938                          : [
    939                              {
    940                                className: "button is-rounded is-small",
    941                                text: "Add",
    942                                onClick: resultsUi.showAddFilterLabel
    943                              }
    944                            ]
    945                      )
    946                  }
    947                }
    948              }
    949            ]
    950          });
    951          return filters;
    952        },
    953        createSessionsTable(
    954          tableType,
    955          sessions,
    956          { pinned = false, static = false } = {}
    957        ) {
    958          const getTagStyle = status => {
    959            switch (status) {
    960              case "completed":
    961                return "is-success";
    962              case "running":
    963                return "is-info";
    964              case "aborted":
    965                return "is-danger";
    966              case "paused":
    967                return "is-warning";
    968              case "pending":
    969                return "is-primary";
    970            }
    971          };
    972          var sortIcon = null;
    973          if (tableType in sortDetail) {
    974            sortIcon = sortDetail[tableType].ascending
    975              ? "fas fa-sort-down"
    976              : "fas fa-sort-up";
    977          }
    978          return UI.createElement({
    979            element: "table",
    980            className: "table is-bordered is-hoverable is-fullwidth",
    981            children: [
    982              {
    983                element: "thead",
    984                children: {
    985                  element: "tr",
    986                  children: [
    987                    {
    988                      element: "td",
    989                      style: "text-decoration: underline dotted;",
    990                      text: "Cp",
    991                      className: "is-narrow",
    992                      title: "Select for comparison"
    993                    },
    994                    {
    995                      element: "td",
    996                      text: "Token",
    997                      className: "is-narrow",
    998                      onclick: () => resultsUi.sortSessions(tableType, "token"),
    999                      style: "cursor: pointer;",
   1000                      children: [
   1001                        {
   1002                          element: "i",
   1003                          className: sortIcon,
   1004                          style:
   1005                            "padding-left: 20px; visibility:" +
   1006                            (sortIcon &&
   1007                            sortDetail[tableType].sortColumn == "token"
   1008                              ? "visible;"
   1009                              : "hidden;")
   1010                        }
   1011                      ]
   1012                    },
   1013                    {
   1014                      element: "td",
   1015                      text: "Browser",
   1016                      onclick: () =>
   1017                        resultsUi.sortSessions(tableType, "browser"),
   1018                      style: "cursor: pointer;",
   1019                      className: "is-narrow",
   1020                      children: [
   1021                        {
   1022                          element: "i",
   1023                          className: sortIcon,
   1024                          style:
   1025                            "padding-left: 20px; visibility:" +
   1026                            (sortIcon &&
   1027                            sortDetail[tableType].sortColumn == "browser"
   1028                              ? "visible;"
   1029                              : "hidden;")
   1030                        }
   1031                      ]
   1032                    },
   1033                    {
   1034                      element: "td",
   1035                      text: "Status",
   1036                      onclick: () =>
   1037                        resultsUi.sortSessions(tableType, "status"),
   1038                      style: "cursor: pointer",
   1039                      className: "is-narrow",
   1040                      children: [
   1041                        {
   1042                          element: "i",
   1043                          className: sortIcon,
   1044                          style:
   1045                            "padding-left: 20px; visibility:" +
   1046                            (sortIcon &&
   1047                            sortDetail[tableType].sortColumn == "status"
   1048                              ? "visible;"
   1049                              : "hidden;")
   1050                        }
   1051                      ]
   1052                    },
   1053                    {
   1054                      element: "td",
   1055                      text: "Date Started",
   1056                      onclick: () =>
   1057                        resultsUi.sortSessions(tableType, "dateStarted"),
   1058                      style: "cursor: pointer;",
   1059                      className: "is-narrow",
   1060                      children: [
   1061                        {
   1062                          element: "i",
   1063                          className: sortIcon,
   1064                          style:
   1065                            "padding-left: 20px; visibility:" +
   1066                            (sortIcon &&
   1067                            sortDetail[tableType].sortColumn == "dateStarted"
   1068                              ? "visible;"
   1069                              : "hidden;")
   1070                        }
   1071                      ]
   1072                    },
   1073                    {
   1074                      element: "td",
   1075                      text: "Labels",
   1076                      style: "cursor: pointer; width: 18rem"
   1077                    },
   1078                    static
   1079                      ? null
   1080                      : {
   1081                          element: "td",
   1082                          text: "RefS",
   1083                          title: "Reference Sessions",
   1084                          style: "text-decoration: underline dotted;",
   1085                          className: "is-narrow"
   1086                        },
   1087                    static
   1088                      ? null
   1089                      : {
   1090                          element: "td",
   1091                          colspan: 2,
   1092                          text: "Options",
   1093                          className: "is-narrow"
   1094                        }
   1095                  ]
   1096                }
   1097              },
   1098              {
   1099                element: "tbody",
   1100                children: sessions.map(session => ({
   1101                  element: "tr",
   1102                  className: resultsUi.isSessionDisabled(session)
   1103                    ? "disabled-row"
   1104                    : "",
   1105                  style: "cursor: pointer",
   1106                  onclick: () => resultsUi.openSessionResult(session.token),
   1107                  children: [
   1108                    {
   1109                      element: "td",
   1110                      onclick: event => event.stopPropagation(),
   1111                      style: "vertical-align: middle;",
   1112                      children: [
   1113                        {
   1114                          element: "input",
   1115                          className: "checkbox",
   1116                          style:
   1117                            "width: 18px; height: 18px; margin-top: 0.55em",
   1118                          type: "checkbox",
   1119                          disabled: !resultsUi.isSessionValidForComparison(
   1120                            session
   1121                          ),
   1122                          checked: resultsUi.isSessionSelectedForComparison(
   1123                            session
   1124                          ),
   1125                          onchange: event =>
   1126                            event.target.checked
   1127                              ? resultsUi.addSessionToComparison(session.token)
   1128                              : resultsUi.removeSessionFromComparison(
   1129                                  session.token
   1130                                )
   1131                        }
   1132                      ]
   1133                    },
   1134                    {
   1135                      element: "td",
   1136                      className: "is-family-monospace",
   1137                      style: "vertical-align: middle;",
   1138                      text: session.token.split("-").shift()
   1139                    },
   1140                    {
   1141                      element: "td",
   1142                      style: "vertical-align: middle; white-space: nowrap",
   1143                      text: session.browser.name + " " + session.browser.version
   1144                    },
   1145                    {
   1146                      element: "td",
   1147                      style: "vertical-align: middle; text-align: center",
   1148                      children: [
   1149                        {
   1150                          className: `tag ${getTagStyle(session.status)}`,
   1151                          text: session.status
   1152                        }
   1153                      ]
   1154                    },
   1155                    {
   1156                      element: "td",
   1157                      style: "vertical-align: middle; white-space: nowrap",
   1158                      text: session.dateStarted
   1159                        ? new Date(session.dateStarted).toLocaleString()
   1160                        : "not started"
   1161                    },
   1162                    {
   1163                      element: "td",
   1164                      children: {
   1165                        className: "tags field is-grouped isgrouped-multiline",
   1166                        style: "min-width: 10em",
   1167                        children: session.labels.map(label => ({
   1168                          className: "control",
   1169                          children: {
   1170                            element: "span",
   1171                            className: "tag is-info",
   1172                            text: label
   1173                          }
   1174                        }))
   1175                      }
   1176                    },
   1177                    static
   1178                      ? null
   1179                      : {
   1180                          element: "td",
   1181                          title: session.referenceTokens
   1182                            .map(token => token.split("-").shift())
   1183                            .sort((tokenA, tokenB) => tokenA - tokenB)
   1184                            .join("\n"),
   1185                          style: "white-space:nowrap",
   1186                          children: (() => {
   1187                            const tokens = session.referenceTokens.slice();
   1188                            let overflow = 0;
   1189                            if (tokens.length > 3) {
   1190                              overflow = tokens.length - 2;
   1191                            }
   1192                            if (overflow > 0) tokens.splice(2, overflow + 2);
   1193                            const children = tokens.map(token => {
   1194                              let icon = "";
   1195                              const session = resultsUi.state.sessions[token];
   1196                              switch (session.browser.name.toLowerCase()) {
   1197                                case "firefox":
   1198                                  icon = "fab fa-firefox";
   1199                                  break;
   1200                                case "edge":
   1201                                  icon = "fab fa-edge";
   1202                                  break;
   1203                                case "chrome":
   1204                                case "chromium":
   1205                                  icon = "fab fa-chrome";
   1206                                  break;
   1207                                case "safari":
   1208                                case "webkit":
   1209                                  icon = "fab fa-safari";
   1210                                  break;
   1211                              }
   1212                              return {
   1213                                element: "span",
   1214                                style:
   1215                                  "margin-right: 5px; vertical-align: middle;",
   1216                                children: { element: "i", className: icon }
   1217                              };
   1218                            });
   1219 
   1220                            if (overflow > 0)
   1221                              children.push({
   1222                                element: "span",
   1223                                style: "vertical-align: middle",
   1224                                className: "is-size-7",
   1225                                text: `+${overflow}`
   1226                              });
   1227 
   1228                            return children;
   1229                          })()
   1230                        },
   1231                    static
   1232                      ? null
   1233                      : {
   1234                          element: "td",
   1235                          style: "vertical-align: middle; text-align: center",
   1236                          className: "is-paddingless",
   1237                          children: [
   1238                            {
   1239                              className: "button is-dark is-outlined is-small",
   1240                              title: pinned ? "Unpin session" : "Pin session",
   1241                              style: "margin: 5px",
   1242                              children: [
   1243                                {
   1244                                  element: "span",
   1245                                  className: "icon",
   1246                                  children: [
   1247                                    {
   1248                                      element: "i",
   1249                                      className: "fas fa-thumbtack",
   1250                                      style: pinned
   1251                                        ? ""
   1252                                        : "transform: rotate(45deg)"
   1253                                    }
   1254                                  ]
   1255                                }
   1256                              ],
   1257                              onclick: event => {
   1258                                event.stopPropagation();
   1259                                if (pinned) {
   1260                                  resultsUi.unpinSession(session.token);
   1261                                } else {
   1262                                  resultsUi.pinSession(session.token);
   1263                                }
   1264                              }
   1265                            }
   1266                          ]
   1267                        },
   1268                    static
   1269                      ? null
   1270                      : {
   1271                          element: "td",
   1272                          style: "vertical-align: middle; text-align: center",
   1273                          className: "is-paddingless",
   1274                          children: [
   1275                            {
   1276                              className: "button is-dark is-outlined is-small",
   1277                              title: "Remove session from list",
   1278                              style: "margin: 5px",
   1279                              children: [
   1280                                {
   1281                                  element: "span",
   1282                                  className: "icon",
   1283                                  children: [
   1284                                    {
   1285                                      element: "i",
   1286                                      className: "fas fa-trash-alt"
   1287                                    }
   1288                                  ]
   1289                                }
   1290                              ],
   1291                              onclick: event => {
   1292                                event.stopPropagation();
   1293                                resultsUi.removeSession(session.token);
   1294                              }
   1295                            }
   1296                          ]
   1297                        }
   1298                  ]
   1299                }))
   1300              }
   1301            ]
   1302          });
   1303        },
   1304        updateCompareButton: () => {
   1305          const compareButton = UI.getElement("compare-button");
   1306          if (resultsUi.isComparisonValid()) {
   1307            compareButton.removeAttribute("disabled");
   1308          } else {
   1309            compareButton.setAttribute("disabled", true);
   1310          }
   1311        }
   1312      };
   1313    </script>
   1314  </body>
   1315 </html>