Skip to content

Commit

Permalink
[🖥️+⚡️] - Beginning pagination logic. some DB model creation
Browse files Browse the repository at this point in the history
Signed-off-by: Binyamin Yawitz <[email protected]>
  • Loading branch information
byawitz committed Mar 18, 2024
1 parent 6bd5297 commit e8a841d
Show file tree
Hide file tree
Showing 21 changed files with 294 additions and 85 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# App level
ENVIRONMENT=debug
DEFAULT_LOCAL=en
PAGINATION_SIZE=50
TOKEN_DRIVER=local
APP_SSL_KEY=randomVeryLongKey
API_ENDPOINT=/v1/api
Expand Down
45 changes: 45 additions & 0 deletions apps/dashboard/src/components/data/Table.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<template>
<Container :isXL="true">
<div class="row">
<div class="col">
<div class="card">
<div class="card-body">
<div class="table-responsive">
<div class="table-loading" v-if="tableLoading">
<div class="tl-inner-text">
<div class="spinner-border text-blue" role="status"></div>
<p>{{ $t('Just a moment') }}</p>
</div>
</div>
<table class="table table-vcenter card-table">
<thead>
<tr>
<th v-for="heading in headings" :key="heading.title" :class="{ 'w-1': heading.isNarrow }">{{ heading.title }}</th>
</tr>
</thead>
<tbody>
<slot></slot>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</Container>
</template>

<script setup lang="ts">
import Container from '@/components/layouts/Container.vue';
import { type PropType } from 'vue';
interface Heading {
title: string;
isNarrow: boolean;
}
defineProps({
tableLoading: { default: false, type: Boolean },
headings: { type: Array as PropType<Heading[]> }
});
</script>
22 changes: 22 additions & 0 deletions apps/dashboard/src/components/data/TablePagination.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<template>
<Container :isXL="true" class="mt-3">
<div class="row">
<div class="col d-flex justify-content-between">
<Button @click="hasPrev && emit('prev')" width="w-25" :class="{ disabled: !hasPrev }">{{ $t('tr-Prev') }}</Button>
<Button @click="hasNext && emit('next')" width="w-25" :class="{ disabled: !hasNext }">{{ $t('tr-Next') }}</Button>
</div>
</div>
</Container>
</template>

<script setup lang="ts">
import Container from '@/components/layouts/Container.vue';
import Button from '@/components/form/Button.vue';
const emit = defineEmits(['prev', 'next']);
defineProps({
hasPrev: { default: false, type: Boolean },
hasNext: { default: true, type: Boolean }
});
</script>
2 changes: 1 addition & 1 deletion apps/dashboard/src/components/view/Sidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
</template>
</NavItem>

<NavItem :text="$t('Profile')" href="profile">
<NavItem :text="$t('Profile')" href="/profile">
<template #icon>
<IconUser class="icon" />
</template>
Expand Down
68 changes: 68 additions & 0 deletions apps/dashboard/src/heplers/CursorPaginator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { Ref } from 'vue';
import NetworkHelper from '@/heplers/NetworkHelper';
import type { Router } from 'vue-router';

export default class CursorPaginator {
private items: Ref<any[]>;
private readonly url: string;
private hasNext: Ref<boolean>;
private hasPrev: Ref<boolean>;
private tableLoading: Ref<boolean>;
private router: Router;

constructor(url: string, items: Ref<any[]>, tableLoading: Ref<boolean>, hasNext: Ref<boolean>, hasPrev: Ref<boolean>, router: Router) {
this.url = url;
this.items = items;
this.tableLoading = tableLoading;
this.hasPrev = hasPrev;
this.hasNext = hasNext;
this.router = router;
}

public async next() {
this.tableLoading.value = true;

await this.loadData(CursorPaginator.getURLParam('next'));

this.tableLoading.value = false;
}

public async prev() {
this.tableLoading.value = true;
const id = parseInt(CursorPaginator.getURLParam('prev')) + parseInt(CursorPaginator.getURLParam('size'));

await this.loadData(id);

this.tableLoading.value = false;
}

public async loadData(lastId: any = 0) {
try {
const res = await NetworkHelper.get(`${this.url}${lastId}/`);

if (res.success) {
const data = res.data;

this.items.value = data.items;
this.hasPrev.value = data.hasPrev;
this.hasNext.value = data.hasNext;
const next = data.items[data.items.length - 1].id;
const size = data.size;

await this.router.replace(`${this.router.currentRoute.value.path}?next=${next}&prev=${lastId}&size=${size}`);
}
} catch (e) {
// TODO: Toast for error
}
}

private static getURLParam(param: string) {
const url = new URL(window.location.href.replace('#', ''));

return url.searchParams.get(param) ?? '0';
}

async init() {
await this.loadData(CursorPaginator.getURLParam('prev'));
}
}
2 changes: 2 additions & 0 deletions apps/dashboard/src/heplers/NetworkHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export default class NetworkHelper {
static readonly whoAmI = '/whoami';
static readonly server = '/server';
static readonly links = '/links/';
static readonly linksAll = '/links/all/';
static readonly linkStats = '/links/stat/';

static readonly updateProfile = '/user/update';
Expand Down Expand Up @@ -51,4 +52,5 @@ export default class NetworkHelper {
const base = import.meta.env.MODE === 'development' ? 'http://localhost:8081/v1/api' : `${window.location.origin}/v1/api`;
return `${base}${appendUrl}`;
}

}
135 changes: 59 additions & 76 deletions apps/dashboard/src/views/LinksView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,75 +6,46 @@
</PageHeader>

<div class="page-body">
<Container :isXL="true">
<div class="row">
<div class="col">
<div class="card">
<div class="card-body">
<div class="table-responsive">
<div class="table-loading" v-if="tableLoading">
<div class="tl-inner-text">
<div class="spinner-border text-blue" role="status"></div>
<p>{{ $t('Just a moment') }}</p>
</div>
</div>
<table class="table table-vcenter card-table">
<thead>
<tr>
<th class="w-1">#</th>
<th>{{ $t('Title') }}</th>
<th>{{ $t('Short') }}</th>
<th class="w-1">{{ $t('Clicks') }}</th>
<th>{{ $t('Destination') }}</th>
<th class="w-1"></th>
</tr>
</thead>
<tbody>
<tr v-for="link in links" :key="link.id">
<td>{{ link.id }}</td>
<td class="text-secondary">
<RouterLink :to="`/links/${link.id}`">{{ link.title }}</RouterLink>
</td>
<td class="text-secondary">
<div class="badge py-2">
{{ link.short }}

<a href="#" @click.prevent="copyToClipboard(link)" class="px-2">
<IconCopy :size="18" v-if="!link.copying" />
<template v-else>{{ $t('copied') }}</template>
</a>

<a :href="getShort(link)" target="_blank">
<IconExternalLink :size="18" />
</a>
</div>
</td>
<td class="text-secondary">{{ parseInt(link.clicks.toString()).toLocaleString() }}</td>
<td class="text-secondary">
<span :title="link.dest">{{ link.dest.substring(0, 50) }}{{ link.dest.length > 50 ? '...' : '' }}</span>
</td>
<td>
<div class="btn-list flex-nowrap">
<div class="dropdown">
<button class="btn dropdown-toggle align-text-top" data-bs-toggle="dropdown" aria-expanded="false">
<IconSettings :size="15" />
</button>
<div class="dropdown-menu dropdown-menu-end" style="">
<RouterLink class="dropdown-item" :to="`/links/${link.id}/edit`">{{ $t('Edit') }}</RouterLink>
<a class="text-danger dropdown-item" @click.prevent="askToDeleteLink(link)" href="#"> {{ $t('Delete') }} </a>
</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<Table :table-loading="tableLoading" :headings="headings">
<tr v-for="link in links" :key="link.id">
<td>{{ link.id }}</td>
<td class="text-secondary">
<RouterLink :to="`/links/${link.id}`">{{ link.title }}</RouterLink>
</td>
<td class="text-secondary">
<div class="badge py-2">
{{ link.short }}

<a href="#" @click.prevent="copyToClipboard(link)" class="px-2">
<IconCopy :size="18" v-if="!link.copying" />
<template v-else>{{ $t('copied') }}</template>
</a>

<a :href="getShort(link)" target="_blank">
<IconExternalLink :size="18" />
</a>
</div>
</td>
<td class="text-secondary">{{ parseInt(link.clicks.toString()).toLocaleString() }}</td>
<td class="text-secondary">
<span :title="link.dest">{{ link.dest.substring(0, 50) }}{{ link.dest.length > 50 ? '...' : '' }}</span>
</td>
<td>
<div class="row">
<div class="col d-flex gap-2">
<RouterLink :to="`/links/${link.id}/edit`" class="btn btn-icon btn-sm btn-outline-primary">
<IconPencil :size="14" />
</RouterLink>

<a @click.prevent="askToDeleteLink(link)" href="#" class="btn btn-icon btn-sm btn-outline-danger">
<IconTrash :size="14" />
</a>
</div>
</div>
</div>
</div>
</Container>
</td>
</tr>
</Table>
<TablePagination :hasNext="hasNext" :hasPrev="hasPrev" @next="paginator.next()" @prev="paginator.prev()" />
</div>

<input type="hidden" name="tmp-link-holder" id="tmp-link-holder" />
Expand All @@ -83,29 +54,41 @@
<script setup lang="ts">
import type LinkModel from '@@/db/LinkModel';
import { inject, onMounted, ref, type Ref } from 'vue';
import { IconSettings, IconCopy, IconExternalLink } from '@tabler/icons-vue';
import { IconCopy, IconExternalLink, IconPencil, IconTrash } from '@tabler/icons-vue';
import Button from '@/components/form/Button.vue';
import NetworkHelper from '@/heplers/NetworkHelper';
import PageHeader from '@/components/layouts/PageHeader.vue';
import type { SweetAlertResult, SweetAlertCustomClass } from 'sweetalert2';
import type { SweetAlertResult } from 'sweetalert2';
import { useAppStore } from '@/stores/user';
import Container from '@/components/layouts/Container.vue';
import { useI18n } from 'vue-i18n';
import Table from '@/components/data/Table.vue';
import TablePagination from '@/components/data/TablePagination.vue';
import CursorPaginator from '@/heplers/CursorPaginator';
import { useRouter } from 'vue-router';
const store = useAppStore();
const router = useRouter();
const { t } = useI18n();
const links: Ref<LinkModel[]> = ref([]);
const tableLoading = ref(false);
const hasPrev = ref(false);
const hasNext = ref(false);
const swal: any = inject('$swal');
const paginator = new CursorPaginator(NetworkHelper.linksAll, links, tableLoading, hasNext, hasPrev, router);
const headings = [
{ title: '#', isNarrow: true },
{ title: t('Title'), isNarrow: false },
{ title: t('Short'), isNarrow: false },
{ title: t('Clicks'), isNarrow: true },
{ title: t('Destination'), isNarrow: false },
{ title: '', isNarrow: true }
];
onMounted(async () => {
try {
const res = await NetworkHelper.get(NetworkHelper.links);
if (res.success) links.value = res.data;
} catch (e) {
// TODO: Toast for error
}
await paginator.init();
});
async function deleteLink(deletingLink: LinkModel) {
Expand Down
9 changes: 6 additions & 3 deletions apps/linkos/src/http/api/Links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import API from "../../services/API.ts";
import Link, {type MaybeLink} from "../../models/db/Link.ts";
import RedisProvider from "@/providers/RedisProvider.ts";
import ClickhouseProvider from "@/providers/ClickhouseProvider.ts";
import Global from "@/utils/Global.ts";
import Env from "@/utils/Env.ts";

export default class Links {
public static async add(c: Context) {
Expand Down Expand Up @@ -49,14 +51,15 @@ export default class Links {
}

public static async list(c: Context) {
// TODO: pagination
const links = await Link.getAll();
const last_id = Global.ParseOrValue(c.req.param().last_id);

const links = await Link.getAll(Env.PAGINATION_SIZE, last_id);

if (!links) {
return c.json(API.response(false));
}

return c.json(API.response(true, links));
return c.json(API.response(true, Global.paginationObject(links, Env.PAGINATION_SIZE, last_id)));
}

public static async patch(c: Context) {
Expand Down
1 change: 1 addition & 0 deletions apps/linkos/src/install/PostgresTables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ export default class PostgresTables {
destination_url TEXT NOT NULL,
secret TEXT NOT NULL,
method VARCHAR(15) NOT NULL,
content_type webhookcontenttype NOT NULL,
headers TEXT,
Expand Down
14 changes: 10 additions & 4 deletions apps/linkos/src/models/db/Link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,19 @@ export default class Link extends LinkModel {
return false;
}

public static async getAll() {
public static async getAll(pageSize = 51, lastId = 0) {

try {
const values = [];
if (lastId !== 0) {
values.push(lastId);
}

const pg = PostgresProvider.getClient();
// TODO pagination
const res = await pg?.query<Link>(`SELECT id, title, short, dest, clicks
FROM links
ORDER BY id DESC`);
FROM links ${lastId !== 0 ? 'WHERE id < $1' : ''}
ORDER BY id DESC
LIMIT ${pageSize + 1}`, values);
if (res) {
return res.rows;
}
Expand Down
Loading

0 comments on commit e8a841d

Please sign in to comment.