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
158 changes: 60 additions & 98 deletions frontend/src/components/BookStatus.vue
Original file line number Diff line number Diff line change
@@ -1,89 +1,50 @@
<template>
<v-tooltip v-if="iconOnly" location="bottom">
<template #activator="{ props: tooltipProps }">
<span v-bind="tooltipProps">
<v-icon
v-if="isErrored"
size="small"
color="error"
icon="mdi-alert-circle-outline"
></v-icon>
<v-icon
v-else-if="isProcessing"
size="small"
color="grey"
icon="mdi-clock-outline"
></v-icon>
<v-icon v-else-if="isDeleted" size="small" color="grey-darken-2" icon="mdi-delete"></v-icon>
<v-icon
v-else-if="isToBeDeleted"
size="small"
color="warning"
icon="mdi-delete-clock"
></v-icon>
<v-icon
v-else-if="isMovingFiles"
size="small"
color="info"
icon="mdi-truck-delivery-outline"
></v-icon>
<v-icon
v-else-if="!hasTitle"
size="small"
color="warning"
icon="mdi-alert-circle-outline"
></v-icon>
<v-icon v-else size="small" color="success" icon="mdi-check-circle"></v-icon>
</span>
</template>
<div>
<div class="font-weight-bold">{{ statusLabel }}</div>
<div class="text-caption">{{ locationLabel }}</div>
<div
class="d-flex ga-1 py-md-1 ga-md-0"
:class="
forceRow ? 'flex-row justify-start' : 'flex-row flex-md-column justify-end justify-md-end'
"
>
<div class="d-flex align-center" :class="forceRow ? 'mb-0 mr-2' : 'mb-1 mb-md-0 mr-md-2'">
<template v-if="isErrored">
<v-icon size="small" color="error" icon="mdi-alert-circle-outline"></v-icon>
<span class="text-caption ml-1">Errored</span>
</template>
<template v-else-if="isProcessing">
<v-icon size="small" color="grey" icon="mdi-clock-outline"></v-icon>
<span class="text-caption ml-1">Processing</span>
</template>
<template v-else-if="isDeleted">
<v-icon size="small" color="grey-darken-2" icon="mdi-delete"></v-icon>
<span class="text-caption ml-1">Deleted</span>
</template>
<template v-else-if="isToBeDeleted">
<v-icon size="small" color="warning" icon="mdi-delete-clock"></v-icon>
<span class="text-caption ml-1">To Be Deleted</span>
</template>
<template v-else-if="isMovingFiles">
<v-icon size="small" color="info" icon="mdi-truck-delivery-outline"></v-icon>
<span class="text-caption ml-1">Moving Files</span>
</template>
<template v-else-if="!hasTitle">
<v-icon size="small" color="warning" icon="mdi-alert-circle-outline"></v-icon>
<span class="text-caption ml-1">Pending Title</span>
</template>
<template v-else>
<v-icon size="small" color="success" icon="mdi-check-circle"></v-icon>
<span class="text-caption ml-1">Published</span>
</template>
</div>
</v-tooltip>

<span v-else>
<span v-if="isErrored">
<v-icon size="small" color="error" icon="mdi-alert-circle-outline"></v-icon>
<span class="text-caption ml-1">Errored</span>
</span>
<span v-else-if="isProcessing">
<v-icon size="small" color="grey" icon="mdi-clock-outline"></v-icon>
<span class="text-caption ml-1">Processing</span>
</span>
<span v-else-if="isDeleted">
<v-icon size="small" color="grey-darken-2" icon="mdi-delete"></v-icon>
<span class="text-caption ml-1">Deleted</span>
</span>
<span v-else-if="isToBeDeleted">
<v-icon size="small" color="warning" icon="mdi-delete-clock"></v-icon>
<span class="text-caption ml-1">To Be Deleted</span>
<v-chip size="x-small" class="ml-1" :color="locationColor">
{{ locationLabel }}
</v-chip>
</span>
<span v-else-if="isMovingFiles">
<v-icon size="small" color="info" icon="mdi-truck-delivery-outline"></v-icon>
<span class="text-caption ml-1">Moving Files</span>
<v-chip size="x-small" class="ml-1" :color="locationColor">
{{ locationLabel }}
</v-chip>
</span>
<span v-else-if="!hasTitle">
<v-icon size="small" color="warning" icon="mdi-alert-circle-outline"></v-icon>
<span class="text-caption ml-1">Pending Title</span>
<v-chip size="x-small" class="ml-1" :color="locationColor">
{{ locationLabel }}
</v-chip>
</span>
<span v-else>
<v-icon size="small" color="success" icon="mdi-check-circle"></v-icon>
<span class="text-caption ml-1">Published</span>
<v-chip size="x-small" class="ml-1" :color="locationColor">
{{ locationLabel }}
</v-chip>
</span>
</span>
<v-chip
v-if="showLocationChip"
size="x-small"
:color="locationColor"
variant="flat"
class="align-self-start"
>
{{ locationLabel }}
</v-chip>
</div>
</template>

<script setup lang="ts">
Expand All @@ -93,10 +54,10 @@ import type { Book, BookLight } from '@/types/book'
const props = withDefaults(
defineProps<{
book: Book | BookLight
iconOnly?: boolean
forceRow?: boolean
}>(),
{
iconOnly: false,
forceRow: false,
},
)

Expand All @@ -113,14 +74,15 @@ const isMovingFiles = computed(
)
const hasTitle = computed(() => props.book.title_id)

const statusLabel = computed(() => {
if (isErrored.value) return 'Errored'
if (isDeleted.value) return 'Deleted'
if (isToBeDeleted.value) return 'To Be Deleted'
if (isProcessing.value) return 'Processing'
if (isMovingFiles.value) return 'Moving Files'
if (!hasTitle.value) return 'Pending Title'
return 'Published'
const showLocationChip = computed(() => {
// If the evaluated status is 'Errored' or 'Processing', we want to show the location chip
// even if the location is 'deleted' or 'to_delete' so the user knows where the errored/processing book is.
if (isErrored.value) return true
if (isProcessing.value) return true
// Otherwise, if the status evaluates exactly to 'Deleted' or 'To Be Deleted', we hide the redundant chip.
if (isDeleted.value) return false
if (isToBeDeleted.value) return false
return true
})

const locationLabel = computed(() => {
Expand All @@ -143,15 +105,15 @@ const locationLabel = computed(() => {
const locationColor = computed(() => {
switch (props.book.location_kind) {
case 'quarantine':
return 'grey'
return 'warning'
case 'staging':
return 'info'
return 'secondary'
case 'prod':
return 'success'
case 'to_delete':
return 'warning'
return 'info'
case 'deleted':
return 'grey-darken-2'
return 'red-lighten-1'
default:
return 'grey'
}
Expand Down
110 changes: 78 additions & 32 deletions frontend/src/components/BookTable.vue
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
<template>
<div>
<v-card v-if="!errors.length" :class="{ loading: loading }" flat>
<v-data-table-server
:headers="headers"
<component
:is="isServerSide ? 'v-data-table-server' : 'v-data-table'"
:headers="computedHeaders"
:items="books"
:loading="loading"
:page="props.paginator.page"
:items-per-page="props.paginator.limit"
:items-length="paginator.count"
:items-per-page-options="limits"
:page="isServerSide ? props.paginator.page : undefined"
:items-per-page="isServerSide ? props.paginator.limit : -1"
:items-length="isServerSide ? paginator.count : undefined"
:items-per-page-options="isServerSide ? limits : undefined"
:mobile="smAndDown"
:density="smAndDown ? 'compact' : 'comfortable'"
class="elevation-1"
item-value="name"
@update:options="onUpdateOptions"
:hide-default-footer="props.paginator.count === 0"
:hide-default-header="props.paginator.count === 0"
class="elevation-1 book-table"
item-value="id"
hover
@update:options="isServerSide ? onUpdateOptions($event) : undefined"
@click:row="handleRowClick"
:hide-default-footer="isServerSide ? props.paginator.count === 0 : true"
:hide-default-header="isServerSide ? props.paginator.count === 0 : false"
>
<template #loading>
<div class="d-flex flex-column align-center justify-center pa-8">
Expand All @@ -27,23 +30,42 @@
</template>

<template #[`item.id`]="{ item }">
<router-link :to="{ name: 'book-detail', params: { id: item.id } }">
<router-link :to="{ name: 'book-detail', params: { id: item.id } }" @click.stop>
{{ item.id }}
</router-link>
</template>

<template #[`item.location_kind`]="{ item }">
<v-chip :color="locationKindColors[item.location_kind]" size="small" variant="flat">
{{ item.location_kind }}
</v-chip>
<template #[`item.name`]="{ item }">
<span v-if="item.name">{{ item.name }}</span>
<span v-else class="text-grey">-</span>
</template>

<template #[`item.flavour`]="{ item }">
<span v-if="item.flavour">{{ item.flavour }}</span>
<span v-else class="text-grey">-</span>
</template>

<template #[`item.date`]="{ item }">
<span v-if="item.date">{{ item.date }}</span>
<span v-else class="text-grey">-</span>
</template>

<template #[`item.status`]="{ item }">
<BookStatus :book="item" />
</template>

<template #[`item.deletion_date`]="{ item }">
{{ item.deletion_date ? formatDt(item.deletion_date) : '-' }}
{{ item.deletion_date ? formatDt(item.deletion_date, 'ff') : '-' }}
</template>

<template #[`item.urls`]="{ item }">
<ZimUrlButtons
v-if="showUrls && zimUrls"
:urls="zimUrls[item.id]"
:loading="loadingUrls"
:compact="true"
empty-text=""
/>
</template>

<template #no-data>
Expand All @@ -52,18 +74,20 @@
<div class="text-h6 text-grey-darken-1 mb-2">No books found</div>
</div>
</template>
</v-data-table-server>
</component>
</v-card>
</div>
</template>

<script setup lang="ts">
import BookStatus from '@/components/BookStatus.vue'
import ZimUrlButtons from '@/components/ZimUrlButtons.vue'
import type { Paginator } from '@/types/base'
import { formatDt } from '@/utils/format'
import type { BookLight, LocationKind } from '@/types/book'
import type { BookLight, ZimUrl } from '@/types/book'
import { useRouter, useRoute } from 'vue-router'
import { useDisplay } from 'vuetify'
import { computed } from 'vue'

const router = useRouter()
const route = useRoute()
Expand All @@ -74,22 +98,32 @@ const { smAndDown } = useDisplay()
interface Props {
headers: { title: string; value: string }[]
books: BookLight[]
paginator: Paginator
loading: boolean
errors: string[]
loadingText: string
paginator?: Paginator
loading?: boolean
errors?: string[]
loadingText?: string
filters?: {
id: string
location_kind: string
flag: string
}
showFilters?: boolean
isServerSide?: boolean
showUrls?: boolean
zimUrls?: Record<string, ZimUrl[]>
loadingUrls?: boolean
}

const props = withDefaults(defineProps<Props>(), {
paginator: () => ({ page: 1, limit: 20, skip: 0, count: 0, page_size: 20 }),
loading: false,
errors: () => [],
loadingText: 'Fetching books...',
filters: () => ({ id: '', location_kind: '', flag: '' }),
showSelection: true,
showFilters: true,
isServerSide: true,
showUrls: false,
loadingUrls: false,
})

// Define emits
Expand All @@ -98,17 +132,14 @@ const emit = defineEmits<{
loadData: [limit: number, skip: number]
}>()

const limits = [10, 20, 50, 100]
const computedHeaders = computed(() => {
return props.headers
})

const locationKindColors: Record<LocationKind, string> = {
quarantine: 'warning',
staging: 'secondary',
prod: 'success',
to_delete: 'info',
deleted: 'red-lighten-1',
}
const limits = [10, 20, 50, 100]

function onUpdateOptions(options: { page: number; itemsPerPage: number }) {
if (!props.isServerSide) return
const query = { ...route.query }

if (options.page > 1) {
Expand All @@ -123,4 +154,19 @@ function onUpdateOptions(options: { page: number; itemsPerPage: number }) {
emit('limitChanged', options.itemsPerPage)
}
}

function handleRowClick(event: Event, { item }: { item: BookLight }) {
// Prevent navigation if the user clicked on a link or button
const target = event.target as HTMLElement
if (target.closest('a') || target.closest('button')) {
return
}
router.push({ name: 'book-detail', params: { id: item.id } })
}
</script>

<style scoped>
.book-table :deep(tbody tr) {
cursor: pointer;
}
</style>
2 changes: 1 addition & 1 deletion frontend/src/views/BookView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
<div class="text-subtitle-2">Status</div>
</v-col>
<v-col cols="12" md="9">
<BookStatus :book="book" />
<BookStatus :book="book" force-row />
</v-col>
</v-row>
<v-divider class="my-2"></v-divider>
Expand Down
Loading