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

mozilla / blurts-server / 45ba77d7-fd3c-4064-90eb-157d6aa10611

pending completion
45ba77d7-fd3c-4064-90eb-157d6aa10611

push

circleci

Robert Helmer
update the queue adr

282 of 1631 branches covered (17.29%)

Branch coverage included in aggregate %.

959 of 4375 relevant lines covered (21.92%)

3.65 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
      window.gtag('event', 'resolved_breach_item', {
×
61
        action: e.target.checked ? 'resolved' : 'unresolved',
×
62
        page_location: location.href,
63
        data_class: e.target.value
64
      })
65
      break
×
66
    case e.type === 'email-added':
67
      state.emailCount = e.detail.newEmailCount
×
68
      renderZeroState()
×
69
      break
×
70
  }
71
}
72

73
/**
74
 * @param {HTMLInputElement} input
75
 */
76
async function updateBreachStatus (input) {
77
  const affectedEmail = state.selectedEmail
×
78
  const breachId = input.name
×
79
  const checkedInputs = Array.from(input.closest('.resolve-list').querySelectorAll('input:checked'))
×
80
  input.disabled = true
×
81

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

96
    if (!res.ok) throw new Error('Bad fetch response')
×
97

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

112
function renderResolvedCounts () {
113
  state.resolvedCount = breachesPartial.querySelectorAll(`[data-status='resolved'][data-email='${state.selectedEmail}']`).length
×
114
  state.unresolvedCount = breachesPartial.querySelectorAll(`[data-status='unresolved'][data-email='${state.selectedEmail}']`).length
×
115
  resolvedCountOutput.textContent = state.resolvedCount
×
116
  unresolvedCountOutput.textContent = state.unresolvedCount
×
117
}
118

119
function renderBreachRows () {
120
  let delay = 0
×
121
  let hidden
122

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

134
function renderZeroState () {
135
  let temp
136

137
  breachesTable.querySelector('.zero-state')?.remove()
×
138
  statusFilter.toggleAttribute('disabled', state.resolvedCount === 0 && state.unresolvedCount === 0)
×
139

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

152
  const content = temp.content.cloneNode(true)
×
153
  content.querySelector('.current-email').textContent = state.selectedEmail
×
154
  content.querySelector('.add-email-cta').toggleAttribute('hidden', state.emailCount >= state.emailTotal)
×
155
  breachesTable.append(content)
×
156
}
157

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

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

197
  pieChart.setAttribute('data', JSON.stringify(chartData))
×
198
}
199

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

209
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