Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor <DateFilter> #2960

Merged
merged 1 commit into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 4 additions & 14 deletions assets/js/common/DateFilter/DateFilter.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { Fragment, useState, useRef } from 'react';
import classNames from 'classnames';
import { Transition } from '@headlessui/react';
import { format as formatDate } from 'date-fns-tz';

import useOnClickOutside from '@hooks/useOnClickOutside';
import { EOS_CLOSE, EOS_CHECK } from 'eos-icons-react';
Expand All @@ -16,9 +17,7 @@ const preconfiguredOptions = {
};

const toHumanDate = (date) =>
date &&
date instanceof Date &&
`${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
date && date instanceof Date && formatDate(date, 'MM/dd/yyyy hh:mm:ss a');

const renderOptionItem = (option, placeholder) => {
if (!option || !Array.isArray(option)) {
Expand Down Expand Up @@ -74,22 +73,13 @@ function Tick() {
}

function DateTimeInput({ value, onChange }) {
const valueToDate = (v) => {
const tzoffset = new Date().getTimezoneOffset() * 60000;
const date = new Date(`${v}:00.000Z`);
return new Date(date.getTime() + tzoffset);
};
const dateToValue = (date) => {
const tzoffset = new Date().getTimezoneOffset() * 60000;
const newDate = new Date(date.getTime() - tzoffset);
return newDate.toISOString().split('.')[0];
};
const dateToValue = (date) => formatDate(date, "yyyy-MM-dd'T'HH:mm:ss.SSS");

return (
<Input
value={value && dateToValue(value)}
onChange={(e) => {
onChange(valueToDate(e.target.value));
onChange(new Date(`${e.target.value}`));
}}
type="datetime-local"
/>
Expand Down
36 changes: 36 additions & 0 deletions assets/js/common/DateFilter/DateFilter.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,40 @@ describe('DateFilter component', () => {

expect(mockOnChange).toHaveBeenCalledWith(['30d ago', anyDate]);
});

it('should select a custom date when typed into the input field', async () => {
const user = userEvent.setup();
const mockOnChange = jest.fn();
const { container } = render(
<DateFilter title="by date" prefilled onChange={mockOnChange} />
);
await act(() => user.click(screen.getByText('Filter by date...')));
const input = container.querySelector('input[type="datetime-local"]');
await act(() => user.type(input, '2024-08-14T10:21'));

expect(mockOnChange).toHaveBeenCalledWith([
'custom',
new Date('2024-08-14T10:21'),
]);
});

it.each`
value | expected
${new Date('2024-08-14T15:21:00')} | ${'08/14/2024 03:21:00 PM'}
${'2021-01-24T05:50:23'} | ${'01/24/2021 05:50:23 AM'}
`('should render the custom date ($value)', async ({ value, expected }) => {
const mockOnChange = jest.fn();

render(
<DateFilter
title="by date"
value={['custom', value]}
prefilled
onChange={mockOnChange}
/>
);

expect(screen.getByText(expected)).toBeInTheDocument();
expect(mockOnChange).not.toHaveBeenCalled();
});
});
6 changes: 4 additions & 2 deletions assets/js/pages/ActivityLogPage/searchParams.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

import { uniq } from 'lodash';
import { pipe, map, reduce, defaultTo } from 'lodash/fp';
import { fromZonedTime, toZonedTime } from 'date-fns-tz';
import { format as formatDate, toZonedTime } from 'date-fns-tz';

const toUTC = (date) => formatDate(date, "yyyy-MM-dd'T'HH:mm:ss.000'Z'");

const omitUndefined = (obj) =>
Object.fromEntries(
Expand Down Expand Up @@ -63,7 +65,7 @@ export const filterValueToSearchParams = pipe(
switch (k) {
case 'from_date':
case 'to_date':
return [k, [v[0], fromZonedTime(new Date(v[1])).toISOString()]];
return [k, [v[0], toUTC(new Date(v[1]))]];
default:
return [k, v];
}
Expand Down
141 changes: 73 additions & 68 deletions assets/js/pages/ActivityLogPage/searchParams.test.js
Original file line number Diff line number Diff line change
@@ -1,95 +1,100 @@
import { fromZonedTime } from 'date-fns-tz';
import {
filterValueToSearchParams,
searchParamsToAPIParams,
searchParamsToFilterValue,
} from './searchParams';

describe('searchParams helpers', () => {});
describe('searchParamsToAPIParams', () => {
it('should convert search params to API params', () => {
const nowUTC = '2024-08-01T17:23:00.000Z';
describe('searchParams helpers', () => {
describe('searchParamsToAPIParams', () => {
it('should convert search params to API params', () => {
const nowUTC = '2024-08-01T17:23:00.000Z';

const sp = new URLSearchParams();
sp.append('from_date', 'custom');
sp.append('from_date', nowUTC);
sp.append('type', 'login_attempt');
sp.append('type', 'resource_tagging');
const sp = new URLSearchParams();
sp.append('from_date', 'custom');
sp.append('from_date', nowUTC);
sp.append('type', 'login_attempt');
sp.append('type', 'resource_tagging');

const result = searchParamsToAPIParams(sp);
const result = searchParamsToAPIParams(sp);

expect(result).toEqual({
from_date: nowUTC,
type: ['login_attempt', 'resource_tagging'],
expect(result).toEqual({
from_date: nowUTC,
type: ['login_attempt', 'resource_tagging'],
});
});
});
});

describe('searchParamsToFilterValue', () => {
it('should convert search params to filter value', () => {
const nowUTC = '2024-08-01T17:23:00.000Z';
describe('searchParamsToFilterValue', () => {
it('should convert search params to filter value', () => {
const nowUTC = '2024-08-01T17:23:00.000Z';
const now = new Date(nowUTC);

const sp = new URLSearchParams();
sp.append('from_date', 'custom');
sp.append('from_date', nowUTC);
sp.append('type', 'login_attempt');
sp.append('type', 'resource_tagging');
const sp = new URLSearchParams();
sp.append('from_date', 'custom');
sp.append('from_date', nowUTC);
sp.append('type', 'login_attempt');
sp.append('type', 'resource_tagging');

const result = searchParamsToFilterValue(sp);
const result = searchParamsToFilterValue(sp);

expect(result).toEqual({
from_date: ['custom', expect.any(Date)],
type: ['login_attempt', 'resource_tagging'],
});
expect(result).toEqual({
from_date: ['custom', expect.any(Date)],
type: ['login_attempt', 'resource_tagging'],
});

expect(result.from_date[1].getHours()).toEqual(17);
expect(result.from_date[1].getTime()).toEqual(
now.getTime() + now.getTimezoneOffset() * 60 * 1000
);
});
});
});

describe('filterValueToSearchParams', () => {
it('should convert filter value to search params', () => {
const now = new Date();
const utcNow = fromZonedTime(now).toISOString();

const filterValue = {
from_date: ['custom', now],
type: ['login_attempt', 'resource_tagging'],
};

const result = filterValueToSearchParams(filterValue);
describe('filterValueToSearchParams', () => {
it('should convert filter value to search params', () => {
const base = '2024-08-14T10:21:00';
const utcNow = `${base}.000Z`;
const now = new Date(base);

expect(result.getAll('from_date')).toEqual(['custom', utcNow]);
expect(result.getAll('type')).toEqual([
'login_attempt',
'resource_tagging',
]);
});

it('should use a fresh URLSearchParams instance', () => {
const now = new Date();
const utcNow = fromZonedTime(now).toISOString();
const filterValue = {
from_date: ['custom', now],
type: ['login_attempt', 'resource_tagging'],
};

const filterValue = {
from_date: ['custom', now],
type: ['login_attempt', 'resource_tagging'],
};
const result = filterValueToSearchParams(filterValue);

// apply two times to test if the function is using a fresh URLSearchParams instance
/* */ filterValueToSearchParams(filterValue);
const result = filterValueToSearchParams(filterValue);
expect(result.getAll('from_date')).toEqual(['custom', utcNow]);
expect(result.getAll('type')).toEqual([
'login_attempt',
'resource_tagging',
]);
});

expect(result.getAll('from_date')).toEqual(['custom', utcNow]);
expect(result.getAll('type')).toEqual([
'login_attempt',
'resource_tagging',
]);
});
it('should use a fresh URLSearchParams instance', () => {
const base = '2024-08-14T10:21:00';
const utcNow = `${base}.000Z`;
const now = new Date(base);

const filterValue = {
from_date: ['custom', now],
type: ['login_attempt', 'resource_tagging'],
};

// apply two times to test if the function is using a fresh URLSearchParams instance
/* */ filterValueToSearchParams(filterValue);
const result = filterValueToSearchParams(filterValue);

expect(result.getAll('from_date')).toEqual(['custom', utcNow]);
expect(result.getAll('type')).toEqual([
'login_attempt',
'resource_tagging',
]);
});

it('should return an instance of URLSearchParams when filters are empty', () => {
const filterValue = {};
it('should return an instance of URLSearchParams when filters are empty', () => {
const filterValue = {};

const result = filterValueToSearchParams(filterValue);
const result = filterValueToSearchParams(filterValue);

expect(result).toEqual(expect.any(URLSearchParams));
expect(result).toEqual(expect.any(URLSearchParams));
});
});
});
6 changes: 3 additions & 3 deletions test/e2e/cypress/e2e/activity_log.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ context('Activity Log page', () => {
);

cy.contains('Login Attempt, Tag Added').should('be.visible');
cy.contains('8/14/2024 10:21:00 AM').should('be.visible');
cy.contains('8/13/2024 10:21:00 AM').should('be.visible');
cy.contains('08/14/2024 10:21:00 AM').should('be.visible');
cy.contains('08/13/2024 10:21:00 AM').should('be.visible');

cy.wait('@data').its('response.statusCode').should('eq', 200);
});
Expand Down Expand Up @@ -60,7 +60,7 @@ context('Activity Log page', () => {
);
});

it('should reset filters', () => {
it('should reset filonlyters', () => {
cy.intercept({
url: '/api/v1/activity_log',
}).as('data');
Expand Down
Loading