Skip to content

Commit

Permalink
Add admin view for unparseable webhooks
Browse files Browse the repository at this point in the history
  • Loading branch information
ledoyen committed Apr 29, 2024
1 parent cb7c78e commit 418e9d6
Show file tree
Hide file tree
Showing 8 changed files with 253 additions and 36 deletions.
1 change: 1 addition & 0 deletions src/css/grid.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
.col-auto {
flex: 0 0 auto;
width: auto;
flex-direction: column;
}

.row-centered {
Expand Down
52 changes: 32 additions & 20 deletions src/lib/api-backend/admin.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,46 @@
import { axiosAPI } from './../axios';
import type { Table, UserForAdmin } from './../types';
import type { Page, Table, UnparseableWebhook, UserForAdmin } from './../types';
import { loadUser } from './../stores';

export const getTables = async (): Promise<Table[]> => {
return await axiosAPI.get<Table[]>('/fapi/admin/table').then((res) => res.data);
return await axiosAPI.get<Table[]>('/fapi/admin/table').then((res) => res.data);
};

export const getUsers = async (): Promise<UserForAdmin[]> => {
return await axiosAPI.get<UserForAdmin[]>('/fapi/admin/user').then((res) => res.data);
return await axiosAPI.get<UserForAdmin[]>('/fapi/admin/user').then((res) => res.data);
};

export const deleteUsers = async (ids: string[]) => {
await axiosAPI({
method: 'delete',
url: '/fapi/admin/user',
data: JSON.stringify(ids),
headers: {
'Content-Type': 'application/json'
}
});
await axiosAPI({
method: 'delete',
url: '/fapi/admin/user',
data: JSON.stringify(ids),
headers: {
'Content-Type': 'application/json'
}
});
};

export const setUsersTeacher = async (ids: string[]) => {
await axiosAPI({
method: 'patch',
url: '/fapi/admin/teacher',
data: JSON.stringify(ids),
headers: {
'Content-Type': 'application/json'
}
});
await loadUser();
await axiosAPI({
method: 'patch',
url: '/fapi/admin/teacher',
data: JSON.stringify(ids),
headers: {
'Content-Type': 'application/json'
}
});
await loadUser();
};

export const getUnparseableWebhooks = async (page: number, per_page: number): Promise<Page<UnparseableWebhook>> => {
return await axiosAPI.get<Page<UnparseableWebhook>>(`/fapi/admin/unparseable_webhooks?page=${page}&per_page=${per_page}`)
.then((res) => res.data);
};

export const deleteUnparseableWebhooks = async () => {
await axiosAPI({
method: 'delete',
url: '/fapi/admin/unparseable_webhooks'
});
};
47 changes: 32 additions & 15 deletions src/lib/api-mock/admin.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,46 @@
import type { Table, UserForAdmin } from './../types';
import type { Page, Table, UnparseableWebhook, UserForAdmin } from './../types';
import * as mock from './../mock';
import { loadUser } from './../stores';

export const getTables = async (): Promise<Table[]> => {
return [
{
name: 'table_1',
row_count: 4
},
{
name: 'table_2',
row_count: 13
}
];
return [
{
name: 'table_1',
row_count: 4
},
{
name: 'table_2',
row_count: 13
}
];
};

export const getUsers = async (): Promise<UserForAdmin[]> => {
return mock.users;
return mock.users;
};

export const deleteUsers = async (ids: string[]) => {
mock.deleteUsers(ids);
mock.deleteUsers(ids);
};

export const setUsersTeacher = async (ids: string[]) => {
mock.users.filter((u) => ids.includes(u.id)).forEach((u) => (u.teacher = true));
await loadUser();
mock.users.filter((u) => ids.includes(u.id)).forEach((u) => (u.teacher = true));
await loadUser();
};

export const getUnparseableWebhooks = async (page: number, per_page: number): Promise<Page<UnparseableWebhook>> => {
const start = page == 1 ? 0 : (page - 1) * per_page;
const end = start + per_page;
const items = mock.unparseable_webhooks.slice(start, end);
return {
page: page,
per_page: per_page,
total_count: mock.unparseable_webhooks.length,
total_page: Math.ceil(mock.unparseable_webhooks.length / per_page),
data: items,
};
};

export const deleteUnparseableWebhooks = async () => {
mock.unparseable_webhooks.splice(0);
}
21 changes: 20 additions & 1 deletion src/lib/mock.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { UserForAdmin, TeacherAssignment } from './types';
import type { UserForAdmin, TeacherAssignment, UnparseableWebhook } from './types';

export const uuidv4 = () => {
return crypto.randomUUID();
Expand Down Expand Up @@ -92,6 +92,25 @@ export let modules: TeacherModuleBacked[] = [
}
];

const inOptions: string = 'abcdefghijklmnopqrstuvwxyz0123456789';
const generateText = (length: number): string => {
let text: string = '';
for (let i = 0; i < length; i++) {
text += inOptions.charAt(Math.floor(Math.random() * inOptions.length));
}
return text;
}

export const unparseable_webhooks: UnparseableWebhook[] = new Array(32);

for (let index = 0; index < unparseable_webhooks.length; index++) {
const text_length = Math.random() * 170 + 30;
const json_value = generateText(text_length)
unparseable_webhooks[index] = {
index: index, created_at: new Date('2023-09-14T14:05:44Z'), origin: 'github', event: 'machin', payload: `{"big json payload": "${json_value}"}`, error: 'some error'
};
}

export const deleteUsers = (ids: string[]): void => {
users = users.filter((u) => !ids.includes(u.id));
};
Expand Down
17 changes: 17 additions & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,20 @@ export type ModuleDesc = {
grade: number;
latest_update?: Date;
};

export type Page<T> = {
page: number;
per_page: number;
total_page: number;
total_count: number;
data: Array<T>;
}

export type UnparseableWebhook = {
index?: number;
created_at: Date;
origin: string;
event: string;
payload: string;
error: string;
}
7 changes: 7 additions & 0 deletions src/routes/admin/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@
href="/admin/database">Database</a
>
</li>
<li class="nav-item">
<a
class="nav-link active"
aria-current={$page.url.pathname === '/admin/unparseable_webhooks' ? 'page' : undefined}
href="/admin/unparseable_webhooks">Unparseable Webhooks</a
>
</li>
</nav>

<slot class="content" />
Expand Down
139 changes: 139 additions & 0 deletions src/routes/admin/unparseable_webhooks/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
<script lang="ts">
import Icon from '@iconify/svelte';
import type { Page, UnparseableWebhook } from '$lib/types';
import api from '$lib/api';
import '$css/table.css';
import '$css/grid.css';
let pageNumber = 1;
let storedPage: Page<UnparseableWebhook>;
async function getItemsAndStoreThem() {
storedPage = await api.getUnparseableWebhooks(pageNumber, 15);
console.dir(storedPage);
return storedPage;
}
let itemPromise = getItemsAndStoreThem();
async function deleteAll() {
await api.deleteUnparseableWebhooks();
itemPromise = getItemsAndStoreThem();
}
async function copy(text: string) {
await navigator.clipboard.writeText(text);
}
async function gotoPrevious() {
pageNumber -= 1;
await getItemsAndStoreThem();
}
async function gotoNext() {
pageNumber += 1;
await getItemsAndStoreThem();
}
</script>

<div class="text-column">
<h3>Users</h3>

{#await itemPromise}
<p class="p-white">...loading items</p>
<!-- eslint-disable @typescript-eslint/no-unused-vars -->
{:then page}
<div class="mb-3 row">
<span class="col-sm-1">Bulk actions:</span>
<button type="button" on:click={deleteAll} class="col-sm-1 btn btn-danger"
>Delete all
</button
>
</div>

<table class="table">
<thead>
<tr>
<th scope="col">Created at</th>
<th scope="col">Origin</th>
<th scope="col">Event</th>
<th scope="col">Error</th>
<th scope="col" class="clipped">Payload</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{#each storedPage.data as item}
<tr>
<td>{item.created_at.toISOString()}</td>
<td>{item.origin}</td>
<td>{item.event}</td>
<td>{item.error}</td>
<td class="clipped">{item.payload}</td>
<td>
<button type="button" class="btn-smooth" aria-label="Copy to clipboard"
on:click={() => copy(item.payload)}>
<Icon icon="mingcute:copy-2-line" inline />
</button>
</td>
</tr>
{/each}
</tbody>
</table>
{#if page.total_page > 1}
<div class="row-centered">
<div class="col-auto">
{#if pageNumber > 1}
<button type="button" class="btn-smooth btn-pagination" aria-label="Previous" on:click={gotoPrevious}>
<Icon icon="grommet-icons:caret-previous" inline />
</button>
{/if}
</div>
<div class="col-auto">
Page {pageNumber}
</div>
<div class="col-auto">
{#if pageNumber < page.total_page}
<button type="button" class="btn-smooth btn-pagination" aria-label="Next" on:click={gotoNext}>
<Icon icon="grommet-icons:caret-next" inline />
</button>
{/if}
</div>
</div>
{/if}
{:catch error}
<p style="color: red">{error.message}</p>
{/await}
</div>

<style>
.clipped {
word-wrap: break-word;
max-width: 150px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.btn-smooth {
background: none;
border: none;
}
.btn-smooth:hover {
background: lightgray;
border-radius: 6px;
}
.btn-pagination {
color: cornflowerblue;
font-size: 43px;
}
.col-auto {
display: flex;
justify-content: center;
}
</style>
5 changes: 5 additions & 0 deletions src/routes/admin/unparseable_webhooks/+page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const csr = true;

// since there's no dynamic data here, we can prerender
// it so that it gets served as a static asset in production
export const prerender = true;

0 comments on commit 418e9d6

Please sign in to comment.