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

mozilla / blurts-server / #12224

pending completion
#12224

push

circleci

Vinnl
Add empty state for the breach list

282 of 1319 branches covered (21.38%)

Branch coverage included in aggregate %.

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

959 of 3522 relevant lines covered (27.23%)

2.21 hits per line

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

0.0
/src/views/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 { getMessage, getLocale } from '../../utils/fluent.js'
6
import AppConstants from '../../app-constants.js'
7

8
function createEmailOptions (data) {
9
  const emails = data.verifiedEmails.map(obj => obj.email)
×
10
  const optionElements = emails.map(email => `<option>${email}</option>`)
×
11

12
  return optionElements.join('')
×
13
}
14

15
function createEmailCTA (count) {
16
  const total = parseInt(AppConstants.MAX_NUM_ADDRESSES)
×
17

18
  if (count >= total) return '' // don't show CTA if additional emails are not available for monitor
×
19

20
  return `<button class='anchor' data-dialog='add-email'>${getMessage('add-email-link')}</button>`
×
21
}
22

23
function createBreachRows (data) {
24
  const locale = getLocale()
×
25
  const shortDate = new Intl.DateTimeFormat(locale, { year: 'numeric', month: '2-digit', day: '2-digit', timeZone: 'UTC' })
×
26
  const shortList = new Intl.ListFormat(locale, { style: 'narrow' })
×
27
  const longDate = new Intl.DateTimeFormat(locale, { dateStyle: 'long', timeZone: 'UTC' })
×
28
  const longList = new Intl.ListFormat(locale, { style: 'long' })
×
29
  const breachRowsHTML = data.verifiedEmails.flatMap(account => {
×
30
    return account.breaches.map(breach => {
×
31
      const isHidden = !account.primary || breach.IsResolved // initial breach hidden state
×
32
      const status = breach.IsResolved ? 'resolved' : 'unresolved'
×
33
      const breachDate = Date.parse(breach.BreachDate)
×
34
      const addedDate = Date.parse(breach.AddedDate)
×
35
      const dataClassesTranslated = breach.DataClasses.map(item => getMessage(item))
×
36
      const description = getMessage('breach-description', {
×
37
        companyName: breach.Title,
38
        breachDate: longDate.format(breachDate),
39
        addedDate: longDate.format(addedDate),
40
        dataClasses: longList.format(dataClassesTranslated)
41
      })
42

43
      return `
×
44
      <details class='breach-row' data-status=${status} data-email=${account.email} ${isHidden ? 'hidden' : ''}>
×
45
        <summary>
46
          <span>${breach.Title}</span>
47
          <span>${shortList.format(dataClassesTranslated)}</span>
48
          <span>
49
            <span class='resolution-badge is-resolved'>${getMessage('column-status-badge-resolved')}</span>
50
            <span class='resolution-badge is-active'>${getMessage('column-status-badge-active')}</span>
51
          </span>
52
          <span>${shortDate.format(addedDate)}</span>
53
        </summary>
54
        <article>
55
          <p>${description}</p>
56
          <p><strong>Resolve this breach:</strong></p>
57
          <ol class='resolve-list'>${createResolveSteps(breach)}</ol>
58
        </article>
59
      </details>
60
      `
61
    })
62
  })
63

64
  return breachRowsHTML.join('')
×
65
}
66

67
function createResolveSteps (breach) {
68
  const checkedArr = breach.ResolutionsChecked || []
×
69
  const resolveStepsHTML = Object.entries(breach.breachChecklist).map(([key, value]) => `
×
70
  <li class='resolve-list-item'>
71
    <input name='${breach.Id}' value='${key}' type='checkbox' ${checkedArr.includes(key) ? 'checked' : ''}>
×
72
    <p>${value.header}<br><i>${value.body}</i></p>
73
  </li>
74
  `)
75

76
  return resolveStepsHTML.join('')
×
77
}
78

79
/**
80
 * @param {*} data
81
 * @param {'none' | 'all-resolved'} status
82
 * @returns string
83
 */
84
function createAddEmailButton (data, status) {
85
  if (data.emailCount >= AppConstants.MAX_NUM_ADDRESSES) {
×
86
    return ''
×
87
  }
88

89
  return `
×
90
    <p>
91
      ${getMessage(`breaches-${status}-cta-blurb`)}
92
    </p>
93
    <button class="primary" data-dialog='add-email'>
94
      ${getMessage(`breaches-${status}-cta-button`)}
95
    </button>
96
  `
97
}
98

99
export const breaches = data => `
×
100
<section>
101
  <header class='breaches-header'>
102
    <h1>${getMessage('breach-heading-email', { 'email-select': `<custom-select name='email-account'>${createEmailOptions(data.breachesData)}</custom-select>` })}</h1>
103
    <figure>
104
      <img src='/images/temp-diagram.webp' width='80' height='80'>
105
      <figcaption class='breach-stats'>
106
        <strong>10 total breaches</strong>
107
        <label>Resolved</label>
108
        <label>Unresolved</label>
109
      </figcaption>
110
    </figure>
111
    <figure class='email-stats'>
112
      <img src='/images/icon-email.svg' width='55' height='30'>
113
      <figcaption>
114
        <strong>${getMessage('emails-monitored', { count: data.emailCount, total: AppConstants.MAX_NUM_ADDRESSES })}</strong>
115
        ${createEmailCTA(data.emailCount)}
116
      </figcaption>
117
    </figure>
118
  </header>
119
</section>
120
<section class='breaches-filter'>
121
  <input id='breaches-unresolved' type='radio' name='breaches-status' value='unresolved' autocomplete='off' checked>
122
  <label for='breaches-unresolved'><output>&nbsp;</output>${getMessage('filter-label-unresolved')}</label>
123
  <input id='breaches-resolved' type='radio' name='breaches-status' value='resolved' autocomplete='off'>
124
  <label for='breaches-resolved'><output>&nbsp;</output>${getMessage('filter-label-resolved')}</label>
125
</section>
126
<section class='breaches-table' data-token=${data.csrfToken}>
127
  <header>
128
    <span>${getMessage('column-company')}</span>
129
    <span>${getMessage('column-breached-data')}</span>
130
    ${
131
      /*
132
       * The active/resolved badge does not have a column header, but by
133
       * including an empty <span>, we can re-use the `nth-child`-based
134
       * selectors for the content columns.
135
       */
136
      '<span></span>'
137
    }
138
    <span>${getMessage('column-detected')}</span>
139
  </header>
140
  ${createBreachRows(data.breachesData)}
141
  <div class="no-unresolved-breaches-message">
142
    <div class="no-breaches-message">
143
      <img src='/images/breaches-none.svg' alt='' width="136" height="102" />
144
      <h2>
145
        ${getMessage('breaches-none-headline')}
146
      </h2>
147
      <p>
148
        ${
149
          data.breachesData.verifiedEmails.map(account => {
150
            return `<span data-email="${account.email}" ${account.primary ? '' : 'hidden'}>${getMessage('breaches-none-copy', { email: `<b>${account.email}</b>` })}</span>`
×
151
          }).join('')
152
        }
153
      </p>
154
      ${createAddEmailButton(data, 'none')}
155
    </div>
156
    <div class="all-breaches-resolved-message">
157
      <img src='/images/breaches-all-resolved.svg' alt='' width="136" height="102" />
158
      <h2>
159
        ${getMessage('breaches-all-resolved-headline')}
160
      </h2>
161
      <p>
162
        ${
163
          data.breachesData.verifiedEmails.map(account => {
164
            return `<span data-email="${account.email}" ${account.primary ? '' : 'hidden'}>${getMessage('breaches-all-resolved-copy', { email: `<b>${account.email}</b>` })}</span>`
×
165
          }).join('')
166
        }
167
      </p>
168
      ${createAddEmailButton(data, 'all-resolved')}
169
    </div>
170
  </div>
171
</section>
172
`
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