Skip to content
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Meta, StoryObj } from "@storybook/react";

import { fn } from "@storybook/test";
import { DaysFilter } from ".";

// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
const meta: Meta<typeof DaysFilter> = {
title: "Errors/GlobalErrorsList/DaysFilter",
component: DaysFilter,
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout
layout: "fullscreen"
}
};

export default meta;

type Story = StoryObj<typeof meta>;

// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
export const Default: Story = {
args: {
onChanged: fn()
}
};
158 changes: 158 additions & 0 deletions src/components/Errors/GlobalErrorsList/DaysFilter/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { ChangeEvent, useCallback, useEffect, useMemo, useState } from "react";
import { usePrevious } from "../../../../hooks/usePrevious";
import { isUndefined } from "../../../../typeGuards/isUndefined";
import { sendUserActionTrackingEvent } from "../../../../utils/actions/sendUserActionTrackingEvent";
import { formatUnit } from "../../../../utils/formatUnit";
import { CalendarIcon } from "../../../common/icons/12px/CalendarIcon";
import { MinusIcon } from "../../../common/icons/MinusIcon";
import { PlusIcon } from "../../../common/icons/PlusIcon";
import { NewPopover } from "../../../common/NewPopover";
import { NewIconButton } from "../../../common/v3/NewIconButton";
import { MenuList } from "../../../Navigation/common/MenuList";
import { trackingEvents } from "../../tracking";
import * as s from "./styles";
import { DaysFilterProps } from "./types";

const MAX_VALUE = 14;
const MIN_VALUE = 1;

const DEFAULT_LIST_OPTIONS = [7, 14];

const getOptionLabel = (days: number) => `${days} ${formatUnit(days, "Day")}`;

export const DaysFilter = ({ onChanged }: DaysFilterProps) => {
const [isDateMenuOpen, setIsDateMenuOpen] = useState(false);
const [selectedDays, setSelectedDays] = useState<number>();
const [currentValue, setCurrentValue] = useState<number>();
const previousSelectedDays = usePrevious(selectedDays);
const handleSelectionChange = useCallback(
(days: number) => {
const value = selectedDays === days ? undefined : days;
setSelectedDays(value);
setCurrentValue(value);
setIsDateMenuOpen(false);
},
[selectedDays]
);

const daysFilterMenuItems = useMemo(
() =>
Object.values(DEFAULT_LIST_OPTIONS).map((x) => ({
id: x.toString(),
label: "Last " + getOptionLabel(x),
isSelected: selectedDays === x,
onClick: () => handleSelectionChange(x)
})),
[handleSelectionChange, selectedDays]
);

useEffect(() => {
if (previousSelectedDays !== selectedDays) {
onChanged(selectedDays);
}
}, [selectedDays, previousSelectedDays, onChanged]);

const handleSortingMenuButtonClick = () => {
sendUserActionTrackingEvent(
trackingEvents.GLOBAL_ERRORS_VIEW_DATES_FILTERS_CHANGE
);
setIsDateMenuOpen(!isDateMenuOpen);
setCurrentValue(selectedDays);
};

const handleCounterInputChange = (e: ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
const intValue = parseInt(newValue);
const days =
!newValue || Number.isNaN(intValue)
? undefined
: intValue > MAX_VALUE
? selectedDays
: intValue;

sendUserActionTrackingEvent(
trackingEvents.GLOBAL_ERRORS_DAYS_FILTER_DECREMENT_CLICKED
);
setCurrentValue(days);
};

const handleDecrement = () => {
if (currentValue === MIN_VALUE) {
return;
}
sendUserActionTrackingEvent(
trackingEvents.GLOBAL_ERRORS_DAYS_FILTER_DECREMENT_CLICKED
);
setCurrentValue(currentValue ? currentValue - 1 : 0);
};

const handleIncrement = () => {
if (currentValue === MAX_VALUE) {
return;
}

sendUserActionTrackingEvent(
trackingEvents.GLOBAL_ERRORS_DAYS_FILTER_INCREMENT_CLICKED
);
setCurrentValue(currentValue ? currentValue + 1 : 1);
};

const handleApplyClick = () => {
sendUserActionTrackingEvent(
trackingEvents.GLOBAL_ERRORS_DAYS_FILTER_APPLY_BTN_CLICKED
);
setSelectedDays(currentValue);
setIsDateMenuOpen(false);
};

return (
<NewPopover
isOpen={isDateMenuOpen}
onOpenChange={setIsDateMenuOpen}
content={
<s.DatePopup>
<s.ItemsContainer>
<MenuList items={daysFilterMenuItems} highlightSelected={true} />
<s.CustomCounterContainer>
<s.Counter>
<NewIconButton
buttonType="secondaryBorderless"
icon={() => <MinusIcon size={16} />}
onClick={handleDecrement}
/>
<s.CounterInput
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider to use input of type=number to leverage validation logic on browser:
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/number

onChange={handleCounterInputChange}
value={currentValue?.toString() ?? ""}
/>
<NewIconButton
buttonType="secondaryBorderless"
icon={() => <PlusIcon size={16} />}
onClick={handleIncrement}
/>
<s.Text>Last days</s.Text>
</s.Counter>
<s.ApplyButton
buttonType="primary"
label="Apply filters"
onClick={handleApplyClick}
/>
</s.CustomCounterContainer>
</s.ItemsContainer>
</s.DatePopup>
}
placement={"bottom-end"}
>
<s.DateButton
$isActive={!isUndefined(selectedDays) && selectedDays > 0}
icon={() => (
<s.ButtonIconContainer>
<CalendarIcon size={12} color={"currentColor"} />
</s.ButtonIconContainer>
)}
label={selectedDays ? getOptionLabel(selectedDays) : "Dates"}
buttonType={"secondary"}
onClick={handleSortingMenuButtonClick}
/>
</NewPopover>
);
};
76 changes: 76 additions & 0 deletions src/components/Errors/GlobalErrorsList/DaysFilter/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import styled from "styled-components";
import {
bodyRegularTypography,
footnoteRegularTypography
} from "../../../common/App/typographies";
import { NewButton } from "../../../common/v3/NewButton";
import { TextField } from "../../../common/v3/TextField";
import { Popup } from "../../../Navigation/common/Popup";
import { DaysButtonProps } from "./types";

export const ButtonIconContainer = styled.div`
color: ${({ theme }) => theme.colors.v3.icon.tertiary};
`;

export const DateButton = styled(NewButton)<DaysButtonProps>`
border: 1px solid
${({ theme, $isActive }) => {
if ($isActive) {
return theme.colors.v3.surface.brandPrimary;
}

return theme.colors.v3.stroke.dark;
}};
background: ${({ theme, $isActive }) => {
if ($isActive) {
return theme.colors.v3.surface.brandDark;
}

return theme.colors.v3.surface.primary;
}};
`;

export const DatePopup = styled(Popup)`
min-width: 164px;
display: flex;
`;

export const ItemsContainer = styled.div`
display: flex;
flex-direction: column;
gap: 8px;
`;

export const CustomCounterContainer = styled.div`
flex-direction: column;
display: flex;
gap: 8px;
margin: 0 -8px -8px;
padding: 8px;
background: ${({ theme }) => theme.colors.v3.surface.highlight};
`;

export const Counter = styled.div`
${bodyRegularTypography}
display: flex;
flex-direction: row;
align-items: center;
`;

export const CounterInput = styled(TextField)`
${footnoteRegularTypography}
padding: 4px;

input {
max-width: 16px;
}
`;

export const ApplyButton = styled(NewButton)`
width: 100%;
justify-content: center;
`;

export const Text = styled.span`
padding-right: 5px;
`;
33 changes: 33 additions & 0 deletions src/components/Errors/GlobalErrorsList/DaysFilter/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ErrorFilter } from "../../../../store/errors/errorsSlice";
import { ButtonProps } from "../../../common/v3/NewButton/types";

export interface GetGlobalErrorsFiltersDataPayload {
environment: string;
filterName?: string;
filterData?: {
services?: string[];
values: string[];
};
}

export interface FilterData<T> {
filterName: ErrorFilter;
values: T[];
}

export interface EndpointFilterData {
spanCodeObjectId: string;
displayName: string;
}

export interface SetGlobalErrorsFiltersDataPayload {
filters: FilterData<string | EndpointFilterData>[];
}

export interface DaysButtonProps extends ButtonProps {
$isActive: boolean;
}

export interface DaysFilterProps {
onChanged: (days?: number) => void;
}
25 changes: 21 additions & 4 deletions src/components/Errors/GlobalErrorsList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { actions } from "../actions";
import { NewErrorCard } from "../NewErrorCard";
import { NoDataEmptyState } from "../NoDataEmptyState";
import { trackingEvents } from "../tracking";
import { DaysFilter } from "./DaysFilter";
import { GlobalErrorsFilters } from "./GlobalErrorsFilters";
import * as s from "./styles";
import {
Expand Down Expand Up @@ -78,7 +79,8 @@ export const GlobalErrorsList = () => {
globalErrorsPageSize: pageSize,
globalErrorsList: list,
globalErrorsTotalCount: totalCount,
globalErrorsSelectedFilters: selectedFilters
globalErrorsSelectedFilters: selectedFilters,
globalErrorsLastDays: lastDays
} = useErrorsSelector();

const previousList = usePrevious(list);
Expand All @@ -90,14 +92,20 @@ export const GlobalErrorsList = () => {
setGlobalErrorsPage,
resetGlobalErrors,
resetGlobalErrorsSelectedFilters,
setGlobalErrorsViewMode
setGlobalErrorsViewMode,
setGlobalErrorsLastDays
} = useStore.getState();

const areGlobalErrorsFiltersEnabled = getFeatureFlagValue(
backendInfo,
FeatureFlag.ARE_GLOBAL_ERRORS_FILTERS_ENABLED
);

const isGlobalErrorsLastDaysFilterEnabled = getFeatureFlagValue(
backendInfo,
FeatureFlag.IS_GLOBAL_ERROR_LAST_DAYS_FILTER_ENABLED
);

const areGlobalErrorsCriticalityAndUnhandledFiltersEnabled =
getFeatureFlagValue(
backendInfo,
Expand Down Expand Up @@ -131,6 +139,7 @@ export const GlobalErrorsList = () => {
searchCriteria: search,
sortBy: sorting,
page,
lastDays,
pageSize: PAGE_SIZE,
dismissed: mode === ViewMode.OnlyDismissed,
...(areGlobalErrorsFiltersEnabled
Expand All @@ -153,6 +162,7 @@ export const GlobalErrorsList = () => {
sorting,
page,
mode,
lastDays,
areGlobalErrorsFiltersEnabled,
selectedFilters.services,
selectedFilters.endpoints,
Expand Down Expand Up @@ -265,6 +275,10 @@ export const GlobalErrorsList = () => {
setGlobalErrorsSearch(search);
};

const handleDayFilterChange = (days?: number) => {
setGlobalErrorsLastDays(days);
};

const handleSortingMenuButtonClick = () => {
sendUserActionTrackingEvent(
trackingEvents.GLOBAL_ERRORS_VIEW_SORTING_CHANGE
Expand Down Expand Up @@ -366,6 +380,9 @@ export const GlobalErrorsList = () => {
<s.ToolbarContainer>
{areGlobalErrorsFiltersEnabled && <GlobalErrorsFilters />}
<SearchInput value={search} onChange={handleSearchInputChange} />
{isGlobalErrorsLastDaysFilterEnabled && (
<DaysFilter onChanged={handleDayFilterChange} />
)}
<NewPopover
isOpen={isSortingMenuOpen}
onOpenChange={setIsSortingMenuOpen}
Expand All @@ -378,9 +395,9 @@ export const GlobalErrorsList = () => {
>
<NewButton
icon={() => (
<s.SortButtonIconContainer>
<s.ButtonIconContainer>
<OppositeArrowsIcon size={12} color={"currentColor"} />
</s.SortButtonIconContainer>
</s.ButtonIconContainer>
)}
label={"Sort"}
buttonType={"secondary"}
Expand Down
2 changes: 1 addition & 1 deletion src/components/Errors/GlobalErrorsList/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export const EmptyStateContent = styled.div`
color: ${({ theme }) => theme.colors.v3.text.tertiary};
`;

export const SortButtonIconContainer = styled.div`
export const ButtonIconContainer = styled.div`
color: ${({ theme }) => theme.colors.v3.icon.tertiary};
`;

Expand Down
Loading
Loading