• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mozilla / blurts-server / #12683

pending completion
#12683

push

circleci

Vinnl
expose selectedIndex for custom-select component

282 of 1420 branches covered (19.86%)

Branch coverage included in aggregate %.

8 of 8 new or added lines in 2 files covered. (100.0%)

959 of 3926 relevant lines covered (24.43%)

2.03 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/client/js/partials/breaches.js
1
/* This Source Code Form is subject to the terms of the Mozilla Public
2
 * License, v. 2.0. If a copy of the MPL was not distributed with this
3
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4

5
import { capitalFirstLetter } from '../utils.js'
6

7
const breachesPartial = document.querySelector("[data-partial='breaches']")
×
8
const chartColors = ['#321C64', '#AB71FF', '#952BB9', '#D74CF0', '#9e9e9e']
×
9
const state = new Proxy({
×
10
  selectedEmail: null,
11
  selectedStatus: 'unresolved',
12
  resolvedCount: null,
13
  unresolvedCount: null,
14
  emailCount: null,
15
  emailTotal: null
16
}, {
17
  set (target, key, value) {
18
    if (target[key] === value) return true
×
19

20
    target[key] = value
×
21
    if (key === 'selectedEmail' || key === 'selectedStatus') render()
×
22
    return true
×
23
  }
24
})
25

26
let breachesTable, breachRows, emailSelect, pieChart, statusFilter, resolvedCountOutput, unresolvedCountOutput
27

28
function init () {
29
  breachesTable = breachesPartial.querySelector('.breaches-table')
×
30
  breachRows = breachesTable.querySelectorAll('.breach-row')
×
31
  emailSelect = breachesPartial.querySelector('.breaches-header custom-select')
×
32
  pieChart = breachesPartial.querySelector('.breaches-header circle-chart')
×
33
  statusFilter = breachesPartial.querySelector('.breaches-filter')
×
34
  resolvedCountOutput = statusFilter.querySelector("label[for='breaches-resolved'] output")
×
35
  unresolvedCountOutput = statusFilter.querySelector("label[for='breaches-unresolved'] output")
×
36

37
  state.emailCount = parseInt(breachesPartial.querySelector('.email-stats').dataset.count)
×
38
  state.emailTotal = parseInt(breachesPartial.querySelector('.email-stats').dataset.total)
×
39
  state.selectedEmail = emailSelect.value // triggers render
×
40

41
  emailSelect.addEventListener('change', handleEvent)
×
42
  statusFilter.addEventListener('change', handleEvent)
×
43
  breachesTable.addEventListener('change', handleEvent)
×
44
  document.body.addEventListener('email-added', handleEvent)
×
45
}
46

47
function handleEvent (e) {
48
  switch (true) {
×
49
    case e.target.matches('custom-select[name="email-account"]'):
50
      state.selectedEmail = e.target.value
×
51
      breachesTable.querySelectorAll('span[data-email]').forEach(message => message.toggleAttribute('hidden', message.dataset.email !== e.target.value))
×
52
      document.cookie = `monitor.selected-email-index=${e.target.selectedIndex}`
×
53
      break
×
54
    case e.target.matches('input[name="breaches-status"]'):
55
      state.selectedStatus = e.target.value
×
56
      statusFilter.dataset.selected = e.target.value
×
57
      break
×
58
    case e.target.matches('.resolve-list-item [type="checkbox"]'):
59
      updateBreachStatus(e.target)
×
60
      break
×
61
    case e.type === 'email-added':
62
      state.emailCount = e.detail.newEmailCount
×
63
      renderZeroState()
×
64
      break
×
65
  }
66
}
67

68
/**
69
 * @param {HTMLInputElement} input
70
 */
71
async function updateBreachStatus (input) {
72
  const affectedEmail = state.selectedEmail
×
73
  const breachId = input.name
×
74
  const checkedInputs = Array.from(input.closest('.resolve-list').querySelectorAll('input:checked'))
×
75
  input.disabled = true
×
76

77
  try {
×
78
    const res = await fetch('/api/v1/user/breaches', {
×
79
      method: 'PUT',
80
      headers: {
81
        'Content-Type': 'application/json',
82
        'x-csrf-token': breachesTable.dataset.token
83
      },
84
      body: JSON.stringify({
85
        affectedEmail,
86
        breachId,
87
        resolutionsChecked: checkedInputs.map(input => input.value)
×
88
      })
89
    })
90

91
    if (!res.ok) throw new Error('Bad fetch response')
×
92

93
    const data = await res.json()
×
94
    input.closest('.breach-row').dataset.status = data[affectedEmail][breachId].isResolved ? 'resolved' : 'unresolved'
×
95
    renderResolvedCounts()
×
96
  } catch (e) {
97
    // TODO: localize error messages
98
    const toast = document.createElement('toast-alert')
×
99
    toast.textContent = 'Could not update breach status: please try again later.'
×
100
    document.body.append(toast)
×
101
    console.error('Could not update user breach resolve status:', e)
×
102
  } finally {
103
    input.disabled = false
×
104
  }
105
}
106

107
function renderResolvedCounts () {
108
  state.resolvedCount = breachesPartial.querySelectorAll(`[data-status='resolved'][data-email='${state.selectedEmail}']`).length
×
109
  state.unresolvedCount = breachesPartial.querySelectorAll(`[data-status='unresolved'][data-email='${state.selectedEmail}']`).length
×
110
  resolvedCountOutput.textContent = state.resolvedCount
×
111
  unresolvedCountOutput.textContent = state.unresolvedCount
×
112
}
113

114
function renderBreachRows () {
115
  let delay = 0
×
116
  let hidden
117

118
  breachRows.forEach(breach => {
×
119
    hidden = (breach.dataset.email !== state.selectedEmail) || (breach.dataset.status !== state.selectedStatus)
×
120
    breach.toggleAttribute('hidden', hidden)
×
121
    breach.removeAttribute('open')
×
122
    if (!hidden) {
×
123
      breach.style.setProperty('--delay', `${delay}ms`)
×
124
      delay += 50
×
125
    }
126
  })
127
}
128

129
function renderZeroState () {
130
  let temp
131

132
  breachesTable.querySelector('.zero-state')?.remove()
×
133
  statusFilter.toggleAttribute('disabled', state.resolvedCount === 0 && state.unresolvedCount === 0)
×
134

135
  switch (true) {
×
136
    case state.resolvedCount === 0 && state.unresolvedCount === 0:
×
137
      temp = breachesPartial.querySelector('template.no-breaches')
×
138
      break
×
139
    case state.resolvedCount > 0 && state.unresolvedCount === 0:
×
140
      if (state.selectedStatus !== 'unresolved') return // only show zero-state on empty unresolved screen
×
141
      temp = breachesPartial.querySelector('template.all-breaches-resolved')
×
142
      break
×
143
    default:
144
      return
×
145
  }
146

147
  const content = temp.content.cloneNode(true)
×
148
  content.querySelector('.current-email').textContent = state.selectedEmail
×
149
  content.querySelector('.add-email-cta').toggleAttribute('hidden', state.emailCount >= state.emailTotal)
×
150
  breachesTable.append(content)
×
151
}
152

153
function renderPieChart () {
154
  const rowsForSelectedEmail = Array.from(breachesTable.querySelectorAll(`[data-email='${state.selectedEmail}']`))
×
155
  const classesForSelectedEmail = rowsForSelectedEmail.flatMap(row => row.dataset.classes.split(','))
×
156
  const classesMap = classesForSelectedEmail.reduce((acc, cur) => {
×
157
    acc.set(cur, (acc.get(cur) ?? 0) + 1) // set count for each class key
×
158
    return acc
×
159
  }, new Map())
160
  const classesTop3 = [...classesMap.keys()].sort((a, b) => classesMap.get(b) - classesMap.get(a)).slice(0, 3)
×
161
  const classesTotal = classesForSelectedEmail.length
×
162
  const chartData = []
×
163

164
  switch (true) {
×
165
    case classesMap.size === 0:
166
      chartData.push({
×
167
        key: pieChart.dataset.txtNone,
168
        name: capitalFirstLetter(pieChart.dataset.txtNone),
169
        count: 1,
170
        color: chartColors[4]
171
      })
172
      break
×
173
    case classesMap.size >= 4:
174
      chartData[3] = {
×
175
        key: pieChart.dataset.txtOther,
176
        name: capitalFirstLetter(pieChart.dataset.txtOther),
177
        count: classesTotal - classesMap.get(classesTop3[0]) - classesMap.get(classesTop3[1]) - classesMap.get(classesTop3[2]),
178
        color: chartColors[3]
179
      }
180
      // falls through
181
    default:
182
      classesTop3.forEach((name, i) => {
×
183
        chartData[i] = {
×
184
          key: name,
185
          name: capitalFirstLetter(name),
186
          count: classesMap.get(name),
187
          color: chartColors[i]
188
        }
189
      })
190
  }
191

192
  pieChart.setAttribute('data', JSON.stringify(chartData))
×
193
}
194

195
function render () {
196
  // render split into separate functions to allow independent trigger
197
  // e.g. if user marks all steps resolved – update the count, but leave the breach in place for further user interaction
198
  renderResolvedCounts()
×
199
  renderBreachRows()
×
200
  renderZeroState()
×
201
  renderPieChart()
×
202
}
203

204
if (breachesPartial) init()
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc