Skip to content

Commit

Permalink
MNTOR-1809 - Add chart into dashboard top banner (#3232)
Browse files Browse the repository at this point in the history
* MNTOR-1809 - Add chart into dashboard top banner

* clean up flex values

* add about exposure num modal

* first pass

* add summary metrics for dashboard

* add types

* todo comment

* review comment

* review comment

* map for localized key

* inject chart data into chart component

* use variable for num email addresses

* fix storybook for dashboard

* switch between pre-scan and post-scan views

* fix breaking test

* rm unused vars

* fix l10n lint error and alphatetically organize vars

* add correct data in dashboard top banner desc

* rm console log

* lint

* lint

* pluralize string

* fix stories

* add total nums (#3252)

* add special character for ×

* arrange colours of chart by odd first, then even

* remove unused var

---------

Co-authored-by: mansaj <jozhou@mozilla.com>
  • Loading branch information
codemist and mansaj authored Jul 26, 2023
1 parent fc2d8a7 commit 0951490
Show file tree
Hide file tree
Showing 16 changed files with 600 additions and 205 deletions.
14 changes: 13 additions & 1 deletion locales-pending/premium.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,19 @@ exposure-chart-legend-heading-type = Exposure
exposure-chart-legend-heading-nr = Number
# Variables:
# $nr (number) - Number of a particular type of exposure found for the user
exposure-chart-legend-value-nr = { $nr }x
exposure-chart-legend-value-nr = { $nr }×
exposure-chart-caption = This chart shows how many times your info is actively exposed.
modal-active-number-of-exposures-title = About your number of active exposures
# Variables:
# $limit (number) - Number of email addresses included in the plan
modal-active-number-of-exposures-part-one =
{ $limit ->
[one] This chart includes the total number of times we found each type of data exposed across all data broker profiles and all data breaches for the { $limit } email address that you are currently monitoring.
*[other] This chart includes the total number of times we found each type of data exposed across all data broker profiles and all data breaches for up to { $limit } email addresses that you are currently monitoring.
}
modal-active-number-of-exposures-part-two = For example, if you have 10 exposures of your phone number, that might mean one phone number is exposed across 10 different sites, or it could mean 2 different phone numbers were exposed across 5 different sites.
modal-active-number-of-exposures-part-three = This chart does not include any exposures that are in-progress of being auto-removed. Once your exposures are fixed, they will be added to your total number of fixed exposures on the Fixed page.
# Here's What We Fixed Progress Card

Expand Down
1 change: 1 addition & 0 deletions locales/en/data-classes.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ financial-investments = Financial investments
financial-transactions = Financial transactions
fitness-levels = Fitness levels
flights-taken = Flights taken
full-names = Full name
genders = Genders
geographic-locations = Geographic locations
government-issued-ids = Government issued IDs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import { View as DashboardEl } from "./View";
import { HibpLikeDbBreach } from "../../../../../../utils/hibp";
import { ScanResult } from "../../../../../functions/server/onerep";
import { Shell } from "../../../Shell";
import { StateAbbr } from "../../../../../../utils/states";
import { getEnL10nSync } from "../../../../../functions/server/mockL10n";
import { createRandomScan } from "../../../../../../apiMocks/mockData";
import { DashboardSummary } from "../../../../../functions/server/dashboard";

const meta: Meta<typeof DashboardEl> = {
title: "Pages/Dashboard",
Expand All @@ -19,33 +19,6 @@ const meta: Meta<typeof DashboardEl> = {
export default meta;
type Story = StoryObj<typeof DashboardEl>;

const _ScanResultMockItem: ScanResult = {
id: 1,
profile_id: 1,
first_name: "John",
last_name: "Doe",
middle_name: "string",
age: `${30}`,
addresses: [
{
city: "123",
state: "State" as StateAbbr,
street: "Street",
zip: "123456",
},
],
phones: [""],
emails: [""],
data_broker: "Familytree.com",
created_at: "11/09/23",
updated_at: "11/09/23",
url: "",
link: "",
relatives: [],
status: "new",
data_broker_id: 0,
};

const BreachMockItem1: HibpLikeDbBreach = {
AddedDate: new Date("2018-11-07T14:48:00.000Z"),
BreachDate: "11/09/23",
Expand Down Expand Up @@ -138,7 +111,57 @@ const breachItemArraySample: HibpLikeDbBreach[] = [
BreachMockItem4,
];

export const Dashboard: Story = {
const dashboardSummaryNoScan: DashboardSummary = {
dataBreachTotalNum: 0,
dataBrokerTotalNum: 51,
totalExposures: 51,
allExposures: {
emailAddresses: 0,
phoneNumbers: 0,
addresses: 0,
familyMembers: 0,
fullNames: 0,
socialSecurityNumbers: 0,
ipAddresses: 0,
passwords: 0,
creditCardNumbers: 0,
pinNumbers: 0,
securityQuestions: 0,
},
sanitizedExposures: [
{ "email-addresses": 30 },
{ "phone-numbers": 19 },
{ "social-security-numbers": 2 },
],
};

const dashboardSummaryWithScan: DashboardSummary = {
dataBreachTotalNum: 88,
dataBrokerTotalNum: 217,
totalExposures: 305,
allExposures: {
emailAddresses: 0,
phoneNumbers: 0,
addresses: 0,
familyMembers: 0,
fullNames: 0,
socialSecurityNumbers: 0,
ipAddresses: 0,
passwords: 0,
creditCardNumbers: 0,
pinNumbers: 0,
securityQuestions: 0,
},
sanitizedExposures: [
{ "physical-addresses": 90 },
{ "family-members-names": 29 },
{ "full-names": 98 },
{ "phone-numbers": 8 },
{ "other-data-class": 80 },
],
};

export const DashboardWithScan: Story = {
render: () => (
<Shell l10n={getEnL10nSync()} session={null}>
<DashboardEl
Expand All @@ -165,7 +188,40 @@ export const Dashboard: Story = {
}}
userScannedResults={scannedResultsArraySample}
locale={"en"}
isUserScannedResults={true}
bannerData={dashboardSummaryWithScan}
/>
</Shell>
),
};

export const DashboardWithoutScan: Story = {
render: () => (
<Shell l10n={getEnL10nSync()} session={null}>
<DashboardEl
user={{ email: "example@example.com" }}
userBreaches={{
emailVerifiedCount: 0,
emailTotalCount: 0,
emailSelectIndex: 0,
ssnBreaches: [],
phoneBreaches: [],
passwordBreaches: [],
breachesData: {
unverifiedEmails: [],
verifiedEmails: [
{
breaches: breachItemArraySample,
email: "test@example.com",
id: 0,
primary: true,
verified: true,
},
],
},
}}
userScannedResults={[]}
locale={"en"}
bannerData={dashboardSummaryNoScan}
/>
</Shell>
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,24 @@ import { it, expect } from "@jest/globals";
import { render } from "@testing-library/react";
import { composeStory } from "@storybook/react";
import { axe } from "jest-axe";
import Meta, { Dashboard } from "./Dashboard.stories";
import Meta, {
DashboardWithScan,
DashboardWithoutScan,
} from "./Dashboard.stories";

jest.mock("next/navigation", () => ({
useRouter: jest.fn(),
usePathname: jest.fn(),
}));

it("passes the axe accessibility test suite", async () => {
const ComposedDashboard = composeStory(Dashboard, Meta);
const ComposedDashboard = composeStory(DashboardWithScan, Meta);
const { container } = render(<ComposedDashboard />);
expect(await axe(container)).toHaveNoViolations();
});

it("passes the axe accessibility test suite", async () => {
const ComposedDashboard = composeStory(DashboardWithoutScan, Meta);
const { container } = render(<ComposedDashboard />);
expect(await axe(container)).toHaveNoViolations();
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,59 @@

.container {
display: flex;
flex-direction: row;
flex-direction: column;
border: 1px solid rgba($color-purple-50, 0.2);
border-radius: $border-radius-md;
padding: $layout-lg $spacing-xl;
padding: $layout-xs;
background-color: $color-white;
background-image: url("./images/dashboard-top-banner-wave.svg");
background-size: cover;
background-position: center;
gap: $layout-xs;
align-items: center;

.content {
@media screen and (min-width: $screen-md) {
padding: $layout-lg $layout-xs;
flex-direction: row;
}

.explainerContentWrapper {
padding: $spacing-sm;
display: flex;
flex-direction: column;
gap: $spacing-md;
align-items: center;

h3 {
font: $text-title-2xs;
@media screen and (min-width: $screen-md) {
padding: $spacing-md;
justify-content: center;
flex: 0.5 1 0%;

@media screen and (min-width: $screen-xl) {
flex: 1 1 0%;
}
}

.cta {
align-self: start;
.explainerContent {
display: flex;
gap: $spacing-md;
flex-direction: column;

@media screen and (min-width: $screen-md) {
max-width: $content-sm;
}

h3 {
font: $text-title-2xs;
}

.cta {
align-self: start;
}
}
}

& > * {
flex: 1 1 0%;
.chart {
@media screen and (min-width: $screen-md) {
flex: 1 1 0%;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,46 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import styles from "./DashboardTopBanner.module.scss";
import { ReactElement } from "react";
import { Button } from "../../../../../components/server/Button";
import { useL10n } from "../../../../../hooks/l10n";
import { useRouter } from "next/navigation";
import { DoughnutChart as Chart } from "../../../../../components/client/Chart";
import { DashboardSummary } from "../../../../../functions/server/dashboard";

export type DashboardTopBannerProps = {
type:
| "LetsFixDataContent"
| "DataBrokerScanUpsellContent"
| "NoExposuresFoundContent"
| "ResumeBreachResolutionContent"
| "YourDataIsProtected";
chart: ReactElement;
| "YourDataIsProtectedContent";
bannerData: DashboardSummary;
};

export const DashboardTopBanner = (props: DashboardTopBannerProps) => {
const l10n = useL10n();
const router = useRouter();

const chartData: [string, number][] = props.bannerData.sanitizedExposures.map(
(obj) => {
const [key, value] = Object.entries(obj)[0];
return [l10n.getString(key), value];
}
);

const contentData = {
LetsFixDataContent: {
headline: l10n.getString("dashboard-top-banner-protect-your-data-title"),
description: l10n.getString(
"dashboard-top-banner-protect-your-data-description",
{
// TODO: Replace all mocked exposure data
data_breach_total_num: 95,
data_broker_total_num: 15,
data_breach_total_num: props.bannerData.dataBreachTotalNum,
data_broker_total_num: props.bannerData.dataBrokerTotalNum,
}
),
cta: {
content: l10n.getString("dashboard-top-banner-protect-your-data-cta"),
onClick: () => {
router.push(
`/redesign/user/dashboard/fix/data-broker-profiles/view-data-brokers`
);
window.location.href =
"/redesign/user/dashboard/fix/data-broker-profiles/view-data-brokers";
},
},
},
Expand Down Expand Up @@ -83,6 +87,7 @@ export const DashboardTopBanner = (props: DashboardTopBannerProps) => {
description: l10n.getString(
"dashboard-top-banner-lets-keep-protecting-description",
{
//TODO: Add remaining total exposures
remaining_exposures_total_num: 40,
}
),
Expand All @@ -95,13 +100,14 @@ export const DashboardTopBanner = (props: DashboardTopBannerProps) => {
},
},
},
YourDataIsProtected: {
YourDataIsProtectedContent: {
headline: l10n.getString(
"dashboard-top-banner-your-data-is-protected-title"
),
description: l10n.getString(
"dashboard-top-banner-your-data-is-protected-description",
{
//TODO: Add original count of exposures
starting_exposure_total_num: 100,
}
),
Expand All @@ -120,20 +126,22 @@ export const DashboardTopBanner = (props: DashboardTopBannerProps) => {

return (
<div className={styles.container}>
<div className={styles.content}>
<div className={styles.explainerContentWrapper}>
{content && (
<>
<div className={styles.explainerContent}>
<h3>{content.headline}</h3>
<p>{content.description}</p>
<span className={styles.cta}>
<Button variant="primary" small onClick={content.cta.onClick}>
{content.cta.content}
</Button>
</span>
</>
</div>
)}
</div>
<div className={styles.chart}>Chart goes here</div>
<div className={styles.chart}>
<Chart data={chartData} />
</div>
</div>
);
};
Loading

0 comments on commit 0951490

Please sign in to comment.