Skip to content

Commit 0db0684

Browse files
authored
Merge pull request #246 from vchurbanov/feature/events-pagination
Feature/events pagination
2 parents 5a0bebe + 49cc8cc commit 0db0684

File tree

20 files changed

+1208
-38
lines changed

20 files changed

+1208
-38
lines changed

src/pages/settings/ui/settings-page-content/settings-page-content.vue

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,28 @@ import { useTitle } from '@vueuse/core'
33
import { storeToRefs } from 'pinia'
44
import { computed } from 'vue'
55
import { THEME_MODES, useSettingsStore } from '@/shared/stores'
6-
import { BadgeNumber, IconSvg } from '@/shared/ui'
6+
import { BadgeNumber, IconSvg, SelectControl } from '@/shared/ui'
77
88
const settingsStore = useSettingsStore()
9-
const { changeTheme, changeNavbar, changeEventCountsVisibility, changeActiveCodeEditor } =
10-
settingsStore
11-
const { themeType, isFixedHeader, isVisibleEventCounts, codeEditor } = storeToRefs(settingsStore)
9+
const {
10+
changeTheme,
11+
changeNavbar,
12+
changeEventCountsVisibility,
13+
setAutoDeleteEventsTime,
14+
changeActiveCodeEditor
15+
} = settingsStore
16+
const { themeType, isFixedHeader, isVisibleEventCounts, autoDeleteEventsTime, codeEditor } =
17+
storeToRefs(settingsStore)
1218
1319
const isDarkMode = computed(() => themeType.value === THEME_MODES.DARK)
1420
21+
const deleteEventsAfter = computed<string>({
22+
get: () => (autoDeleteEventsTime.value === 'none' ? 'none' : String(autoDeleteEventsTime.value)),
23+
set: (val) => {
24+
setAutoDeleteEventsTime(val)
25+
}
26+
})
27+
1528
// TODO: add throttle
1629
const changeCodeEditor = (event: Event) => {
1730
const editor = (event.target as HTMLInputElement).value
@@ -114,6 +127,21 @@ useTitle('Settings | Buggregator')
114127
</div>
115128
</div>
116129

130+
<div class="settings-page-content__title">
131+
Delete Events After:
132+
</div>
133+
134+
<SelectControl
135+
v-model="deleteEventsAfter"
136+
name="delete_events_after"
137+
:options="[
138+
{ value: 'none', text: 'No' },
139+
{ value: '1', text: '1 min' },
140+
{ value: '5', text: '5 min' },
141+
{ value: '10', text: '10 min' }
142+
]"
143+
/>
144+
117145
<div class="settings-page-content__title">
118146
Code Editor Open Link:
119147
</div>

src/shared/lib/io/use-events-requests.ts

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import {storeToRefs} from "pinia";
22
import {useEventsStore, useProfileStore} from "../../stores";
3-
import type { EventId, EventType, ServerEvent } from '../../types';
3+
import type { EventId, EventType, EventTypeCount, EventsPreviewMeta, ServerEvent } from '../../types';
44
import { REST_API_URL } from "./constants";
55

66
type TUseEventsRequests = () => {
77
getAll: () => Promise<ServerEvent<unknown>[]>,
8+
getPreviewPageByType: (
9+
type: EventType,
10+
limit: number,
11+
cursor?: string | null
12+
) => Promise<{ data: ServerEvent<unknown>[]; meta: EventsPreviewMeta | null }>,
13+
getTypeCounts: () => Promise<EventTypeCount[]>,
814
getSingle: (id: EventId) => Promise<ServerEvent<EventType> | null>,
915
deleteAll: () => Promise<void | Response>,
1016
deleteList: (uuids: EventId[]) => Promise<void | Response>,
@@ -22,6 +28,25 @@ export const useEventsRequests: TUseEventsRequests = () => {
2228
const headers = {"X-Auth-Token": token.value }
2329
const getEventRestUrl = (param: string): string => `${REST_API_URL}/api/event/${param}${project.value ? `?project=${project.value}` : ''}`
2430
const getEventsPreviewRestUrl = (): string => `${REST_API_URL}/api/events/preview${project.value ? `?project=${project.value}` : ''}`
31+
const getEventsPreviewByTypeRestUrl = (
32+
type: EventType,
33+
limit: number,
34+
cursor?: string | null
35+
): string => {
36+
const params = new URLSearchParams({
37+
type,
38+
limit: String(limit),
39+
...(project.value ? { project: project.value } : {}),
40+
})
41+
42+
if (cursor) {
43+
params.set('cursor', cursor)
44+
}
45+
46+
return `${REST_API_URL}/api/events/preview?${params.toString()}`
47+
}
48+
const getEventsTypeCountsRestUrl = (): string =>
49+
`${REST_API_URL}/api/events/type-counts${project.value ? `?project=${project.value}` : ''}`
2550

2651
const getAll = () => fetch(getEventsPreviewRestUrl(), { headers })
2752
.then((response) => response.json())
@@ -41,6 +66,45 @@ export const useEventsRequests: TUseEventsRequests = () => {
4166
})
4267
.then((events: ServerEvent<unknown>[]) => events)
4368

69+
const getPreviewPageByType = (type: EventType, limit: number, cursor?: string | null) =>
70+
fetch(getEventsPreviewByTypeRestUrl(type, limit, cursor), { headers })
71+
.then((response) => response.json())
72+
.then((response) => {
73+
if (response?.data) {
74+
return {
75+
data: response.data as ServerEvent<unknown>[],
76+
meta: (response.meta ?? null) as EventsPreviewMeta | null,
77+
}
78+
}
79+
80+
if (response?.code === 403) {
81+
console.error('Forbidden')
82+
return { data: [], meta: null }
83+
}
84+
85+
console.error('Fetch Error')
86+
87+
return { data: [], meta: null }
88+
})
89+
90+
const getTypeCounts = () => fetch(getEventsTypeCountsRestUrl(), { headers })
91+
.then((response) => response.json())
92+
.then((response) => {
93+
if (response?.data) {
94+
return response.data as EventTypeCount[]
95+
}
96+
97+
if (response?.code === 403) {
98+
console.error('Forbidden')
99+
return [];
100+
}
101+
102+
console.error('Fetch Error')
103+
104+
return [];
105+
})
106+
.then((counts: EventTypeCount[]) => counts)
107+
44108
const getSingle = (id: EventId) => fetch(getEventRestUrl(id), {headers})
45109
.then((response) => response.json())
46110
.then((response) => {
@@ -91,6 +155,8 @@ export const useEventsRequests: TUseEventsRequests = () => {
91155

92156
return {
93157
getAll,
158+
getPreviewPageByType,
159+
getTypeCounts,
94160
getSingle,
95161
deleteAll,
96162
deleteList,

src/shared/lib/use-api-transport/use-api-transport.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export const useApiTransport = () => {
2626
const connectionStore = useConnectionStore()
2727
const {
2828
getAll,
29+
getPreviewPageByType,
30+
getTypeCounts,
2931
getSingle,
3032
deleteAll,
3133
deleteList,
@@ -137,6 +139,8 @@ export const useApiTransport = () => {
137139

138140
return {
139141
getEventsAll: getAll,
142+
getEventsPreviewByTypePage: getPreviewPageByType,
143+
getEventsTypeCounts: getTypeCounts,
140144
getEvent: getSingle,
141145
deleteEvent,
142146
deleteEventsAll,
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { storeToRefs } from "pinia";
2+
import { watch } from "vue";
3+
import { useEventsStore, useSettingsStore } from "../../stores";
4+
import type { EventId } from "../../types";
5+
import { useEventsApi } from "./use-events-api";
6+
7+
type AutoDeleteTime = number | "none";
8+
9+
const autoDeleteTimers = new Map<EventId, ReturnType<typeof setTimeout>>();
10+
let autoDeleteInitialized = false;
11+
12+
const toAutoDeleteMs = (val: AutoDeleteTime): number | null => {
13+
if (val === "none") return null;
14+
return val * 60 * 1000;
15+
};
16+
17+
const clearAutoDeleteTimer = (eventId: EventId) => {
18+
const timer = autoDeleteTimers.get(eventId);
19+
if (!timer) return;
20+
globalThis.clearTimeout(timer);
21+
autoDeleteTimers.delete(eventId);
22+
};
23+
24+
const clearAllAutoDeleteTimers = () => {
25+
autoDeleteTimers.forEach((timer) => globalThis.clearTimeout(timer));
26+
autoDeleteTimers.clear();
27+
};
28+
29+
export const ensureAutoDeleteWatcher = () => {
30+
if (autoDeleteInitialized) return;
31+
autoDeleteInitialized = true;
32+
33+
const eventsStore = useEventsStore();
34+
const settingsStore = useSettingsStore();
35+
const eventsApi = useEventsApi();
36+
37+
const { events, lockedIds } = storeToRefs(eventsStore);
38+
const { autoDeleteEventsTime } = storeToRefs(settingsStore);
39+
40+
const schedule = (eventId: EventId) => {
41+
const ms = toAutoDeleteMs(autoDeleteEventsTime.value);
42+
if (ms === null || autoDeleteTimers.has(eventId)) return;
43+
44+
const timer = globalThis.setTimeout(() => {
45+
autoDeleteTimers.delete(eventId);
46+
47+
const locked = lockedIds.value ?? [];
48+
if (locked.includes(eventId)) return;
49+
50+
void eventsApi.removeById(eventId);
51+
}, ms);
52+
53+
autoDeleteTimers.set(eventId, timer);
54+
};
55+
56+
const rescheduleAll = () => {
57+
clearAllAutoDeleteTimers();
58+
59+
const ms = toAutoDeleteMs(autoDeleteEventsTime.value);
60+
if (ms === null) return;
61+
62+
events.value.forEach(({ uuid }) => {
63+
schedule(uuid);
64+
});
65+
};
66+
67+
const syncTimers = (currentIds: Set<EventId>, prevIds: Set<EventId>) => {
68+
prevIds.forEach((id) => {
69+
if (!currentIds.has(id)) {
70+
clearAutoDeleteTimer(id);
71+
}
72+
});
73+
74+
if (toAutoDeleteMs(autoDeleteEventsTime.value) === null) return;
75+
76+
currentIds.forEach((id) => {
77+
if (!prevIds.has(id)) {
78+
schedule(id);
79+
}
80+
});
81+
};
82+
83+
watch(
84+
autoDeleteEventsTime,
85+
() => {
86+
rescheduleAll();
87+
},
88+
{ immediate: true },
89+
);
90+
91+
watch(
92+
() => events.value.map(({ uuid }) => uuid),
93+
(current, prev) => {
94+
const currentIds = new Set(current);
95+
const prevIds = new Set(prev ?? []);
96+
97+
syncTimers(currentIds, prevIds);
98+
},
99+
);
100+
101+
watch(
102+
() => lockedIds.value.slice(),
103+
(current, prev) => {
104+
const currentIds = new Set(current);
105+
const prevIds = new Set(prev ?? []);
106+
107+
const eventsIds = new Set(events.value.map(({ uuid }) => uuid));
108+
const unlockedIds = new Set(
109+
[...prevIds].filter((id) => !currentIds.has(id) && eventsIds.has(id)),
110+
);
111+
112+
syncTimers(currentIds, prevIds);
113+
syncTimers(unlockedIds, new Set<EventId>());
114+
},
115+
);
116+
};

src/shared/lib/use-events/use-events-api.ts

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import { storeToRefs } from "pinia";
22
import type { Ref } from 'vue';
33
import { useEventsStore } from "../../stores";
4-
import type { EventId, EventType, ServerEvent } from '../../types';
4+
import { EventTypes, type EventId, type EventType, type EventsPreviewMeta, type ServerEvent } from '../../types';
55
import { useApiTransport } from '../use-api-transport'
66

77

88
export type TUseEventsApi = {
99
items: Ref<ServerEvent<unknown>[]>
1010
getItem: (id: EventId) => Promise<ServerEvent<EventType> | null>
1111
getAll: () => void
12+
loadMoreByType: (
13+
type: EventType,
14+
cursor?: string | null
15+
) => Promise<{ meta: EventsPreviewMeta | null; data: ServerEvent<unknown>[] }>
1216
removeAll: () => void
1317
removeByType: (type: EventType) => void
1418
removeById: (id: EventId) => void
@@ -26,10 +30,13 @@ export const useEventsApi = (): TUseEventsApi => {
2630
deleteEventsAll,
2731
deleteEventsList,
2832
deleteEventsByType,
29-
getEventsAll,
33+
getEventsPreviewByTypePage,
34+
getEventsTypeCounts,
3035
getEvent,
3136
} = useApiTransport();
3237

38+
const LOADING_PAGE_SIZE = 25;
39+
3340
const removeList = async (uuids: EventId[]) => {
3441
const res = await deleteEventsList(uuids)
3542

@@ -79,22 +86,63 @@ export const useEventsApi = (): TUseEventsApi => {
7986
}
8087

8188
const getAll = () => {
82-
getEventsAll().then((eventsList: ServerEvent<unknown>[]) => {
83-
if (eventsList.length) {
84-
eventsStore.initializeEvents(eventsList);
85-
} else {
86-
// NOTE: clear cached events hardly
87-
eventsStore.removeAll();
88-
}
89-
}).catch((e) => {
90-
console.error(e);
89+
eventsStore.initializeEvents([]);
90+
eventsStore.resetPreviewPagination();
91+
92+
getEventsTypeCounts()
93+
.then((typeCounts) => {
94+
eventsStore.setEventCounts(typeCounts);
95+
})
96+
.catch((e) => {
97+
console.error(e);
98+
eventsStore.resetEventCounts();
99+
});
100+
101+
Object.values(EventTypes).forEach((eventType) => {
102+
getEventsPreviewByTypePage(eventType, LOADING_PAGE_SIZE)
103+
.then(({ data, meta }) => {
104+
if (data.length) {
105+
eventsStore.mergeEvents(data, { updateCounts: false });
106+
}
107+
eventsStore.setPreviewPagination(eventType, meta);
108+
})
109+
.catch((e) => {
110+
console.error(e);
111+
})
91112
})
92113
}
93114

115+
const loadMoreByType = async (eventType: EventType, cursor?: string | null) => {
116+
const pagination = eventsStore.previewPagination[eventType as EventTypes];
117+
if (!pagination?.hasMore) {
118+
return { meta: null, data: [] };
119+
}
120+
121+
const requestCursor = cursor ?? pagination.cursor;
122+
const { data, meta } = await getEventsPreviewByTypePage(
123+
eventType,
124+
LOADING_PAGE_SIZE,
125+
requestCursor,
126+
);
127+
128+
if (data.length) {
129+
eventsStore.mergeEvents(data, { updateCounts: false });
130+
131+
if (eventsStore.cachedIds[eventType]?.length) {
132+
eventsStore.appendCachedIds(eventType, data.map(({ uuid }) => uuid));
133+
}
134+
}
135+
136+
eventsStore.setPreviewPagination(eventType, meta);
137+
138+
return { meta, data };
139+
}
140+
94141
return {
95142
items: events as unknown as Ref<ServerEvent<unknown>[]>,
96143
getItem: getEvent,
97144
getAll,
145+
loadMoreByType,
98146
removeAll,
99147
removeByType,
100148
removeById,

0 commit comments

Comments
 (0)