1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 07:43:35 +00:00

[AC-2508][AC-2511] member access report view and export logic (#10011)

* Added new report card and FeatureFlag for MemberAccessReport

* Add new "isEnterpriseOrgGuard"

* Add member access icon

* Show upgrade organization dialog for enterprise on member access report click

* verify member access featureflag on enterprise org guard

* add comment with TODO information for follow up task

* Initial member access report component

* Improved readability, removed path to wrong component and refactored buildReports to use the productType

* finished MemberAccessReport layout and added temporary service to provide mock data

* Moved member-access-report files to bitwarden_license/
Removed unnecessary files

* Added new tools path on bitwarden_license to the CODEOWNERS file

* added member access description to the messages.json

* layout changes to member access report

* Created new reports-routing under bitwarden_license
Moved member-access-report files to corresponding subfolder

* Added search logic

* Removed routing from member-access-report BL component on OSS.
Added member-access-report navigation to organizations-routing on BL

* removed unnecessary ng-container

* Added OrganizationPermissionsGuard and canAccessReports validation to member-access-report navigation

* replaced deprecated search code with searchControl

* Address PR feedback

* removed unnecessary canAccessReports method

* Added report-utils class with generic functions to support report operations

* Added member access report mock

* Added member access report specific logic

* Splitted code into different classes that explained their objective.

* fixed member access report service test cases

* Addressed PR feedback

* Creating a service to return the data for the member access report

* added missing ExportHelper on index.ts

* Corrected property names on member access report component view

* removed duplicated service
This commit is contained in:
aj-rosado
2024-07-11 17:11:05 +01:00
committed by GitHub
parent a08cbf30d9
commit ea76760782
14 changed files with 713 additions and 56 deletions

View File

@@ -0,0 +1,187 @@
import * as papa from "papaparse";
jest.mock("papaparse", () => ({
unparse: jest.fn(),
}));
import { collectProperty, exportToCSV, getUniqueItems, sumValue } from "./report-utils";
describe("getUniqueItems", () => {
it("should return unique items based on a specified key", () => {
const items = [
{ id: 1, name: "Item 1" },
{ id: 2, name: "Item 2" },
{ id: 1, name: "Item 1 Duplicate" },
];
const uniqueItems = getUniqueItems(items, (item) => item.id);
expect(uniqueItems).toEqual([
{ id: 1, name: "Item 1" },
{ id: 2, name: "Item 2" },
]);
});
it("should return an empty array when input is empty", () => {
const items: { id: number; name: string }[] = [];
const uniqueItems = getUniqueItems(items, (item) => item.id);
expect(uniqueItems).toEqual([]);
});
});
describe("sumValue", () => {
it("should return the sum of all values of a specified property", () => {
const items = [{ value: 10 }, { value: 20 }, { value: 30 }];
const sum = sumValue(items, (item) => item.value);
expect(sum).toBe(60);
});
it("should return 0 when input is empty", () => {
const items: { value: number }[] = [];
const sum = sumValue(items, (item) => item.value);
expect(sum).toBe(0);
});
it("should handle negative numbers", () => {
const items = [{ value: -10 }, { value: 20 }, { value: -5 }];
const sum = sumValue(items, (item) => item.value);
expect(sum).toBe(5);
});
});
describe("collectProperty", () => {
it("should collect a specified property from an array of objects", () => {
const items = [{ values: [1, 2, 3] }, { values: [4, 5, 6] }];
const aggregated = collectProperty(items, "values");
expect(aggregated).toEqual([1, 2, 3, 4, 5, 6]);
});
it("should return an empty array when input is empty", () => {
const items: { values: number[] }[] = [];
const aggregated = collectProperty(items, "values");
expect(aggregated).toEqual([]);
});
it("should handle objects with empty arrays as properties", () => {
const items = [{ values: [] }, { values: [4, 5, 6] }];
const aggregated = collectProperty(items, "values");
expect(aggregated).toEqual([4, 5, 6]);
});
});
describe("exportToCSV", () => {
const data = [
{
email: "john@example.com",
name: "John Doe",
twoFactorEnabled: "On",
accountRecoveryEnabled: "Off",
},
{
email: "jane@example.com",
name: "Jane Doe",
twoFactorEnabled: "On",
accountRecoveryEnabled: "Off",
},
];
test("exportToCSV should correctly export data to CSV format", () => {
const mockExportData = [
{ id: "1", name: "Alice", email: "alice@example.com" },
{ id: "2", name: "Bob", email: "bob@example.com" },
];
const mockedCsvOutput = "mocked CSV output";
(papa.unparse as jest.Mock).mockReturnValue(mockedCsvOutput);
exportToCSV(mockExportData);
const csvOutput = papa.unparse(mockExportData);
expect(csvOutput).toMatch(mockedCsvOutput);
});
it("should map data according to the headers and export to CSV", () => {
const headers = {
email: "Email Address",
name: "Full Name",
twoFactorEnabled: "Two-Step Login",
accountRecoveryEnabled: "Account Recovery",
};
exportToCSV(data, headers);
const expectedMappedData = [
{
"Email Address": "john@example.com",
"Full Name": "John Doe",
"Two-Step Login": "On",
"Account Recovery": "Off",
},
{
"Email Address": "jane@example.com",
"Full Name": "Jane Doe",
"Two-Step Login": "On",
"Account Recovery": "Off",
},
];
expect(papa.unparse).toHaveBeenCalledWith(expectedMappedData);
});
it("should use original keys if headers are not provided", () => {
exportToCSV(data);
const expectedMappedData = [
{
email: "john@example.com",
name: "John Doe",
twoFactorEnabled: "On",
accountRecoveryEnabled: "Off",
},
{
email: "jane@example.com",
name: "Jane Doe",
twoFactorEnabled: "On",
accountRecoveryEnabled: "Off",
},
];
expect(papa.unparse).toHaveBeenCalledWith(expectedMappedData);
});
it("should mix original keys if headers are not fully provided", () => {
const headers = {
email: "Email Address",
};
exportToCSV(data, headers);
const expectedMappedData = [
{
"Email Address": "john@example.com",
name: "John Doe",
twoFactorEnabled: "On",
accountRecoveryEnabled: "Off",
},
{
"Email Address": "jane@example.com",
name: "Jane Doe",
twoFactorEnabled: "On",
accountRecoveryEnabled: "Off",
},
];
expect(papa.unparse).toHaveBeenCalledWith(expectedMappedData);
});
});

View File

@@ -0,0 +1,71 @@
import * as papa from "papaparse";
/**
* Returns an array of unique items from a collection based on a specified key.
*
* @param {T[]} items The array of items to process.
* @param {(item: T) => K} keySelector A function that selects the key to identify uniqueness.
* @returns {T[]} An array of unique items.
*/
export function getUniqueItems<T, K>(items: T[], keySelector: (item: T) => K): T[] {
const uniqueKeys = new Set<K>();
const uniqueItems: T[] = [];
items.forEach((item) => {
const key = keySelector(item);
if (!uniqueKeys.has(key)) {
uniqueKeys.add(key);
uniqueItems.push(item);
}
});
return uniqueItems;
}
/**
* Sums all the values of a specified numeric property in an array of objects.
*
* @param {T[]} array - The array of objects containing the property to be summed.
* @param {(item: T) => number} getProperty - A function that returns the numeric property value for each object.
* @returns {number} - The total sum of the specified property values.
*/
export function sumValue<T>(values: T[], getProperty: (item: T) => number): number {
return values.reduce((sum, item) => sum + getProperty(item), 0);
}
/**
* Collects a specified property from an array of objects.
*
* @param array The array of objects to collect from.
* @param property The property to collect.
* @returns An array of aggregated values from the specified property.
*/
export function collectProperty<T, K extends keyof T, V>(array: T[], property: K): V[] {
const collected: V[] = array
.map((i) => i[property])
.filter((value) => Array.isArray(value))
.flat() as V[];
return collected;
}
/**
* Exports an array of objects to a CSV string.
*
* @param {T[]} data - An array of objects to be exported.
* @param {[key in keyof T]: string } headers - A mapping of keys of type T to their corresponding header names.
* @returns A string in csv format from the input data.
*/
export function exportToCSV<T>(data: T[], headers?: Partial<{ [key in keyof T]: string }>): string {
const mappedData = data.map((item) => {
const mappedItem: { [key: string]: string } = {};
for (const key in item) {
if (headers != null && headers[key as keyof T]) {
mappedItem[headers[key as keyof T]] = String(item[key as keyof T]);
} else {
mappedItem[key] = String(item[key as keyof T]);
}
}
return mappedItem;
});
return papa.unparse(mappedData);
}