diff --git a/web/src/components/event-status-icon/index.test.tsx b/web/src/components/event-status-icon/index.test.tsx
new file mode 100644
index 0000000000..14c0fc628f
--- /dev/null
+++ b/web/src/components/event-status-icon/index.test.tsx
@@ -0,0 +1,30 @@
+import { render } from "~~/test-utils";
+import { EventStatusIcon } from ".";
+import { EventStatus } from "~~/model/event_pb";
+
+describe("EventStatusIcon", () => {
+ it("renders the correct status icon", () => {
+ const { getByTestId } = render(
+
+ );
+ expect(getByTestId("event-not-handled-icon")).toBeInTheDocument();
+ });
+ it("renders the success icon", () => {
+ const { getByTestId } = render(
+
+ );
+ expect(getByTestId("event-success-icon")).toBeInTheDocument();
+ });
+ it("renders the failure icon", () => {
+ const { getByTestId } = render(
+
+ );
+ expect(getByTestId("event-failure-icon")).toBeInTheDocument();
+ });
+ it("renders the outdated icon", () => {
+ const { getByTestId } = render(
+
+ );
+ expect(getByTestId("event-outdated-icon")).toBeInTheDocument();
+ });
+});
diff --git a/web/src/components/event-status-icon/index.tsx b/web/src/components/event-status-icon/index.tsx
index 2f5cb605f3..16a936a9ae 100644
--- a/web/src/components/event-status-icon/index.tsx
+++ b/web/src/components/event-status-icon/index.tsx
@@ -4,7 +4,7 @@ import {
IndeterminateCheckBox,
Block,
} from "@mui/icons-material";
-import { EventStatus } from "~/modules/events";
+import { EventStatus } from "pipecd/web/model/event_pb";
import { FC } from "react";
export interface EventStatusIconProps {
diff --git a/web/src/components/events-page/event-filter/index.tsx b/web/src/components/events-page/event-filter/index.tsx
index 39d7cbb616..0c15281ed8 100644
--- a/web/src/components/events-page/event-filter/index.tsx
+++ b/web/src/components/events-page/event-filter/index.tsx
@@ -9,24 +9,23 @@ import Autocomplete from "@mui/material/Autocomplete";
import { FC, memo, useCallback, useState, useEffect } from "react";
import { FilterView } from "~/components/filter-view";
import { EVENT_STATE_TEXT } from "~/constants/event-status-text";
-import { useAppSelector } from "~/hooks/redux";
+import { Event, EventStatus } from "pipecd/web/model/event_pb";
import {
- Event,
EventFilterOptions,
- EventStatus,
EventStatusKey,
- selectAll as selectAllEvents,
-} from "~/modules/events";
+} from "~/queries/events/use-get-events-infinite";
const ALL_VALUE = "ALL";
export interface EventFilterProps {
+ events: Event.AsObject[];
options: EventFilterOptions;
onClear: () => void;
onChange: (options: EventFilterOptions) => void;
}
export const EventFilter: FC = memo(function EventFilter({
+ events,
options,
onChange,
onClear,
@@ -38,11 +37,8 @@ export const EventFilter: FC = memo(function EventFilter({
[options, onChange]
);
- const events = useAppSelector((state) =>
- selectAllEvents(state.events)
- );
-
const [allNames, setAllNames] = useState(new Array());
+
useEffect(() => {
const names = new Set();
events.map((event) => {
@@ -53,15 +49,17 @@ export const EventFilter: FC = memo(function EventFilter({
const [allLabels, setAllLabels] = useState(new Array());
const [selectedLabels, setSelectedLabels] = useState(new Array());
+
useEffect(() => {
const labels = new Set();
events
.filter((app) => app.labelsMap.length > 0)
- .map((app) => {
- app.labelsMap.map((label) => {
+ .forEach((app) => {
+ app.labelsMap.forEach((label) => {
labels.add(`${label[0]}:${label[1]}`);
});
});
+
setAllLabels(Array.from(labels));
}, [events]);
@@ -69,7 +67,6 @@ export const EventFilter: FC = memo(function EventFilter({
{
onClear();
- setSelectedLabels([]);
}}
>
@@ -78,7 +75,7 @@ export const EventFilter: FC = memo(function EventFilter({
id="filter-event-name"
noOptionsText="No selectable name"
options={allNames}
- value={options.name}
+ value={options.name ?? ""}
onInputChange={(_, value) => {
setAllNames([value]);
}}
diff --git a/web/src/components/events-page/event-item/index.tsx b/web/src/components/events-page/event-item/index.tsx
index 95f8ed4652..59280ab866 100644
--- a/web/src/components/events-page/event-item/index.tsx
+++ b/web/src/components/events-page/event-item/index.tsx
@@ -2,21 +2,18 @@ import { Box, ListItem, Typography, Chip } from "@mui/material";
import dayjs from "dayjs";
import { FC, memo } from "react";
import { EVENT_STATE_TEXT } from "~/constants/event-status-text";
-import { useAppSelector } from "~/hooks/redux";
-import { Event, selectById as selectEventById } from "~/modules/events";
import { EventStatusIcon } from "~/components/event-status-icon";
+import { Event } from "pipecd/web/model/event_pb";
export interface EventItemProps {
- id: string;
+ event: Event.AsObject;
}
const NO_DESCRIPTION = "No description.";
-export const EventItem: FC = memo(function EventItem({ id }) {
- const event = useAppSelector((state) =>
- selectEventById(state.events, id)
- );
-
+export const EventItem: FC = memo(function EventItem({
+ event,
+}) {
if (!event) {
return null;
}
diff --git a/web/src/components/events-page/index.tsx b/web/src/components/events-page/index.tsx
index 954da42603..05b1123b34 100644
--- a/web/src/components/events-page/index.tsx
+++ b/web/src/components/events-page/index.tsx
@@ -11,7 +11,7 @@ import CloseIcon from "@mui/icons-material/Close";
import FilterIcon from "@mui/icons-material/FilterList";
import RefreshIcon from "@mui/icons-material/Refresh";
import dayjs from "dayjs";
-import { FC, useCallback, useEffect, useRef, useState } from "react";
+import { FC, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useInView } from "react-intersection-observer";
import { useNavigate } from "react-router-dom";
import { PAGE_PATH_EVENTS } from "~/constants/path";
@@ -21,20 +21,8 @@ import {
UI_TEXT_REFRESH,
UI_TEXT_MORE,
} from "~/constants/ui-text";
-import {
- useAppDispatch,
- useAppSelector,
- useShallowEqualSelector,
-} from "~/hooks/redux";
-import { fetchApplications } from "~/modules/applications";
-import {
- Event,
- EventFilterOptions,
- fetchEvents,
- fetchMoreEvents,
- selectById as selectEventById,
- selectIds as selectEventIds,
-} from "~/modules/events";
+
+import { Event } from "pipecd/web/model/event_pb";
import { SpinnerIcon } from "~/styles/button";
import {
stringifySearchParams,
@@ -43,42 +31,38 @@ import {
} from "~/utils/search-params";
import { EventFilter } from "./event-filter";
import { EventItem } from "./event-item";
+import {
+ EventFilterOptions,
+ useGetEventsInfinite,
+} from "~/queries/events/use-get-events-infinite";
const sortComp = (a: string | number, b: string | number): number => {
return dayjs(b).valueOf() - dayjs(a).valueOf();
};
-function filterUndefined(value: TValue | undefined): value is TValue {
- return value !== undefined;
-}
-
-const useGroupedEvents = (): Record => {
- const events = useShallowEqualSelector((state) =>
- selectEventIds(state.events)
- .map((id) => selectEventById(state.events, id))
- .filter(filterUndefined)
- );
-
- const result: Record = {};
-
- events.forEach((event) => {
- const dateStr = dayjs(event.createdAt * 1000).format("YYYY/MM/DD");
- if (!result[dateStr]) {
- result[dateStr] = [];
- }
- result[dateStr].push(event);
- });
-
- return result;
+const useNewGroupedEvents = (
+ events: Event.AsObject[]
+): Record => {
+ return useMemo(() => {
+ const result: Record = {};
+
+ events.forEach((event) => {
+ const dateStr = dayjs(event.createdAt * 1000).format("YYYY/MM/DD");
+ if (!result[dateStr]) {
+ result[dateStr] = [];
+ }
+ if (!result[dateStr].some((e) => e.id === event.id)) {
+ result[dateStr].push(event);
+ }
+ });
+
+ return result;
+ }, [events]);
};
export const EventIndexPage: FC = () => {
const navigate = useNavigate();
- const dispatch = useAppDispatch();
const listRef = useRef(null);
- const status = useAppSelector((state) => state.events.status);
- const hasMore = useAppSelector((state) => state.events.hasMore);
- const groupedEvents = useGroupedEvents();
const filterOptions = useSearchParams();
const [openFilter, setOpenFilter] = useState(true);
const [ref, inView] = useInView({
@@ -86,21 +70,33 @@ export const EventIndexPage: FC = () => {
root: listRef.current,
});
- const isLoading = status === "loading";
-
- useEffect(() => {
- dispatch(fetchApplications());
- }, [dispatch]);
+ const {
+ data: eventsData,
+ isFetching,
+ fetchNextPage: fetchMoreEvents,
+ refetch: refetchEvents,
+ isSuccess,
+ } = useGetEventsInfinite(filterOptions);
+
+ const eventsList = useMemo(() => {
+ return eventsData?.pages.flatMap((item) => item.eventsList) || [];
+ }, [eventsData]);
+
+ const hasMore = useMemo(() => {
+ if (!eventsData || eventsData.pages.length === 0) {
+ return false;
+ }
+ const lastIndex = eventsData?.pages.length - 1;
+ return eventsData?.pages?.[lastIndex]?.hasMore || false;
+ }, [eventsData]);
- useEffect(() => {
- dispatch(fetchEvents(filterOptions));
- }, [dispatch, filterOptions]);
+ const groupedEvents = useNewGroupedEvents(eventsList || []);
useEffect(() => {
- if (inView && hasMore && isLoading === false) {
- dispatch(fetchMoreEvents(filterOptions));
+ if (inView && hasMore && isFetching === false) {
+ fetchMoreEvents();
}
- }, [dispatch, inView, hasMore, isLoading, filterOptions]);
+ }, [inView, isFetching, filterOptions, fetchMoreEvents, hasMore]);
// filter handlers
const handleFilterChange = useCallback(
@@ -120,12 +116,12 @@ export const EventIndexPage: FC = () => {
}, [navigate]);
const handleRefreshClick = useCallback(() => {
- dispatch(fetchEvents(filterOptions));
- }, [dispatch, filterOptions]);
+ refetchEvents();
+ }, [refetchEvents]);
const handleMoreClick = useCallback(() => {
- dispatch(fetchMoreEvents(filterOptions));
- }, [dispatch, filterOptions]);
+ fetchMoreEvents();
+ }, [fetchMoreEvents]);
const dates = Object.keys(groupedEvents).sort(sortComp);
@@ -148,10 +144,10 @@ export const EventIndexPage: FC = () => {
color="primary"
startIcon={}
onClick={handleRefreshClick}
- disabled={isLoading}
+ disabled={isFetching}
>
{UI_TEXT_REFRESH}
- {isLoading && }
+ {isFetching && }