feat: consolidate time filtering and add advanced filters
Refactors time-based filtering by combining startAt/endAt into a single time object Adds support for additional filter types in stats API queries Implements deep watching for both time and filter changes Improves code organization and maintainability while enabling more flexible data filtering
This commit is contained in:
parent
ab4fc66da9
commit
2a6b875bdb
11 changed files with 160 additions and 66 deletions
|
|
@ -11,36 +11,38 @@ const defaultData = Object.freeze({
|
|||
const counters = ref(defaultData)
|
||||
|
||||
const id = inject('id')
|
||||
const startAt = inject('startAt')
|
||||
const endAt = inject('endAt')
|
||||
|
||||
const time = inject('time')
|
||||
const filters = inject('filters')
|
||||
async function getLinkCounters() {
|
||||
counters.value = defaultData
|
||||
const { data } = await useAPI('/api/stats/counters', {
|
||||
query: {
|
||||
id: id.value,
|
||||
startAt: startAt.value,
|
||||
endAt: endAt.value,
|
||||
startAt: time.value.startAt,
|
||||
endAt: time.value.endAt,
|
||||
...filters.value,
|
||||
},
|
||||
})
|
||||
counters.value = data?.[0]
|
||||
}
|
||||
|
||||
const stopWatchTime = watch([startAt, endAt], getLinkCounters)
|
||||
const stopWatchQueryChange = watch([time, filters], getLinkCounters, {
|
||||
deep: true,
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
getLinkCounters()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopWatchTime()
|
||||
stopWatchQueryChange()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid gap-4 sm:gap-3 lg:gap-4 sm:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<CardHeader class="flex flex-row justify-between items-center pb-2 space-y-0">
|
||||
<CardTitle class="text-sm font-medium">
|
||||
Visits
|
||||
</CardTitle>
|
||||
|
|
@ -51,7 +53,7 @@ onBeforeUnmount(() => {
|
|||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<CardHeader class="flex flex-row justify-between items-center pb-2 space-y-0">
|
||||
<CardTitle class="text-sm font-medium">
|
||||
Visitors
|
||||
</CardTitle>
|
||||
|
|
@ -62,7 +64,7 @@ onBeforeUnmount(() => {
|
|||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<CardHeader class="flex flex-row justify-between items-center pb-2 space-y-0">
|
||||
<CardTitle class="text-sm font-medium">
|
||||
Referers
|
||||
</CardTitle>
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@ import { now, startOfMonth, startOfWeek } from '@internationalized/date'
|
|||
|
||||
const emit = defineEmits(['update:dateRange'])
|
||||
|
||||
const startAt = inject('startAt')
|
||||
const endAt = inject('endAt')
|
||||
const time = inject('time')
|
||||
|
||||
const dateRange = ref('last-7d')
|
||||
const openCustomDateRange = ref(false)
|
||||
|
|
@ -69,7 +68,7 @@ watch(dateRange, (newValue) => {
|
|||
<SelectTrigger>
|
||||
<SelectValue v-if="dateRange" />
|
||||
<div v-else>
|
||||
{{ shortDate(startAt) }} - {{ shortDate(endAt) }}
|
||||
{{ shortDate(time.startAt) }} - {{ shortDate(time.endAt) }}
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
|
|
|||
63
components/dashboard/Filter.vue
Normal file
63
components/dashboard/Filter.vue
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<script setup>
|
||||
import { Check, ChevronsUpDown } from 'lucide-vue-next'
|
||||
|
||||
const emit = defineEmits(['change'])
|
||||
|
||||
const links = ref([])
|
||||
const isOpen = ref(false)
|
||||
const selectedLinks = ref([])
|
||||
|
||||
async function getLinks() {
|
||||
links.value = await useAPI('/api/link/search')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getLinks()
|
||||
})
|
||||
|
||||
watch(selectedLinks, (value) => {
|
||||
emit('change', 'slug', value.join(','))
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Popover v-model:open="isOpen">
|
||||
<PopoverTrigger as-child>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
:aria-expanded="isOpen"
|
||||
class="flex justify-between w-full sm:w-48"
|
||||
>
|
||||
<div class="flex-1 font-normal text-left truncate" :class="selectedLinks.length ? 'text-foreground' : 'text-muted-foreground'">
|
||||
{{ selectedLinks.length ? selectedLinks.join(', ') : 'Filter Links...' }}
|
||||
</div>
|
||||
<ChevronsUpDown class="ml-2 w-4 h-4 opacity-50 shrink-0" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="p-0 w-full sm:w-48">
|
||||
<Command v-model="selectedLinks" multiple>
|
||||
<CommandInput placeholder="Filter Links..." />
|
||||
<CommandEmpty>No link found.</CommandEmpty>
|
||||
<CommandList>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
v-for="link in links"
|
||||
:key="link.slug"
|
||||
:value="link.slug"
|
||||
@select="isOpen = false"
|
||||
>
|
||||
<Check
|
||||
:class="cn(
|
||||
'mr-2 h-4 w-4',
|
||||
selectedLinks.includes(link.slug) ? 'opacity-100' : 'opacity-0',
|
||||
)"
|
||||
/>
|
||||
{{ link.slug }}
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</template>
|
||||
|
|
@ -8,32 +8,46 @@ defineProps({
|
|||
},
|
||||
})
|
||||
|
||||
const startAt = ref(date2unix(now().subtract({ days: 7 })))
|
||||
const endAt = ref(date2unix(now()))
|
||||
const time = ref({
|
||||
startAt: date2unix(now().subtract({ days: 7 })),
|
||||
endAt: date2unix(now()),
|
||||
})
|
||||
|
||||
provide('startAt', startAt)
|
||||
provide('endAt', endAt)
|
||||
provide('time', time)
|
||||
|
||||
function changeDate(time) {
|
||||
function changeDate(dateRange) {
|
||||
console.log('changeDate', dateRange)
|
||||
// console.log('dashboard date', new Date(time[0] * 1000), new Date(time[1] * 1000))
|
||||
startAt.value = time[0]
|
||||
endAt.value = time[1]
|
||||
time.value.startAt = dateRange[0]
|
||||
time.value.endAt = dateRange[1]
|
||||
}
|
||||
|
||||
const filters = ref({})
|
||||
|
||||
provide('filters', filters)
|
||||
|
||||
function changeFilter(type, value) {
|
||||
console.log('changeFilter', type, value)
|
||||
filters.value[type] = value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="space-y-6">
|
||||
<DashboardNav>
|
||||
<template
|
||||
v-if="link"
|
||||
#left
|
||||
>
|
||||
<h3 class="text-xl font-bold leading-10">
|
||||
{{ link.slug }}'s Stats
|
||||
</h3>
|
||||
</template>
|
||||
<DashboardDatePicker @update:date-range="changeDate" />
|
||||
</DashboardNav>
|
||||
<div class="flex flex-col gap-6 sm:gap-2 sm:flex-row sm:justify-between">
|
||||
<DashboardNav class="flex-1">
|
||||
<template
|
||||
v-if="link"
|
||||
#left
|
||||
>
|
||||
<h3 class="text-xl font-bold leading-10">
|
||||
{{ link.slug }}'s Stats
|
||||
</h3>
|
||||
</template>
|
||||
<DashboardDatePicker @update:date-range="changeDate" />
|
||||
</DashboardNav>
|
||||
<DashboardFilter v-if="!link" @change="changeFilter" />
|
||||
</div>
|
||||
<DashboardCounters />
|
||||
<DashboardViews />
|
||||
<DashboardMetrics />
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ const views = ref([])
|
|||
const chart = computed(() => views.value.length > 1 ? AreaChart : BarChart)
|
||||
|
||||
const id = inject('id')
|
||||
const startAt = inject('startAt')
|
||||
const endAt = inject('endAt')
|
||||
const time = inject('time')
|
||||
const filters = inject('filters')
|
||||
|
||||
const OneDay = 24 * 60 * 60 // 1 day in seconds
|
||||
function getUnit(startAt, endAt) {
|
||||
|
|
@ -22,10 +22,11 @@ async function getLinkViews() {
|
|||
const { data } = await useAPI('/api/stats/views', {
|
||||
query: {
|
||||
id: id.value,
|
||||
unit: getUnit(startAt.value, endAt.value),
|
||||
unit: getUnit(time.value.startAt, time.value.endAt),
|
||||
clientTimezone: getTimeZone(),
|
||||
startAt: startAt.value,
|
||||
endAt: endAt.value,
|
||||
startAt: time.value.startAt,
|
||||
endAt: time.value.endAt,
|
||||
...filters.value,
|
||||
},
|
||||
})
|
||||
views.value = (data || []).map((item) => {
|
||||
|
|
@ -35,19 +36,21 @@ async function getLinkViews() {
|
|||
})
|
||||
}
|
||||
|
||||
const stopWatchTime = watch([startAt, endAt], getLinkViews)
|
||||
const stopWatchQueryChange = watch([time, filters], getLinkViews, {
|
||||
deep: true,
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
getLinkViews()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopWatchTime()
|
||||
stopWatchQueryChange()
|
||||
})
|
||||
|
||||
function formatTime(tick) {
|
||||
if (Number.isInteger(tick) && views.value[tick]) {
|
||||
if (getUnit(startAt.value, endAt.value) === 'hour')
|
||||
if (getUnit(time.value.startAt, time.value.endAt) === 'hour')
|
||||
return views.value[tick].time.split(' ')[1] || ''
|
||||
|
||||
return views.value[tick].time
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import { ChartTooltip } from '@/components/ui/chart'
|
|||
import { VisSingleContainer, VisTopoJSONMap, VisTopoJSONMapSelectors } from '@unovis/vue'
|
||||
|
||||
const id = inject('id')
|
||||
const startAt = inject('startAt')
|
||||
const endAt = inject('endAt')
|
||||
const time = inject('time')
|
||||
const filters = inject('filters')
|
||||
|
||||
const worldMapTopoJSON = ref({})
|
||||
const areaData = ref([])
|
||||
|
|
@ -20,8 +20,9 @@ async function getMapData() {
|
|||
query: {
|
||||
type: 'country',
|
||||
id: id.value,
|
||||
startAt: startAt.value,
|
||||
endAt: endAt.value,
|
||||
startAt: time.value.startAt,
|
||||
endAt: time.value.endAt,
|
||||
...filters.value,
|
||||
},
|
||||
})
|
||||
if (Array.isArray(data)) {
|
||||
|
|
@ -32,7 +33,9 @@ async function getMapData() {
|
|||
}
|
||||
}
|
||||
|
||||
const stopWatchTime = watch([startAt, endAt], getMapData)
|
||||
const stopWatchQueryChange = watch([time, filters], getMapData, {
|
||||
deep: true,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getWorldMapJSON()
|
||||
|
|
@ -40,7 +43,7 @@ onMounted(() => {
|
|||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopWatchTime()
|
||||
stopWatchQueryChange()
|
||||
})
|
||||
|
||||
const valueFormatter = v => v
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ const props = defineProps({
|
|||
})
|
||||
|
||||
const id = inject('id')
|
||||
const startAt = inject('startAt')
|
||||
const endAt = inject('endAt')
|
||||
const time = inject('time')
|
||||
const filters = inject('filters')
|
||||
|
||||
const total = ref(0)
|
||||
const metrics = ref([])
|
||||
|
|
@ -28,8 +28,9 @@ async function getLinkMetrics() {
|
|||
query: {
|
||||
type: props.type,
|
||||
id: id.value,
|
||||
startAt: startAt.value,
|
||||
endAt: endAt.value,
|
||||
startAt: time.value.startAt,
|
||||
endAt: time.value.endAt,
|
||||
...filters.value,
|
||||
},
|
||||
})
|
||||
if (Array.isArray(data)) {
|
||||
|
|
@ -44,14 +45,16 @@ async function getLinkMetrics() {
|
|||
}
|
||||
}
|
||||
|
||||
const stopWatchTime = watch([startAt, endAt], getLinkMetrics)
|
||||
const stopWatchQueryChange = watch([time, filters], getLinkMetrics, {
|
||||
deep: true,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getLinkMetrics()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopWatchTime()
|
||||
stopWatchQueryChange()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
@ -73,7 +76,7 @@ onBeforeUnmount(() => {
|
|||
variant="link"
|
||||
>
|
||||
<Maximize
|
||||
class="w-4 h-4 mr-2"
|
||||
class="mr-2 w-4 h-4"
|
||||
/> DETAILS
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
|
@ -91,7 +94,7 @@ onBeforeUnmount(() => {
|
|||
</CardFooter>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex items-center justify-between h-12 px-4">
|
||||
<div class="flex justify-between items-center px-4 h-12">
|
||||
<Skeleton
|
||||
class="w-32 h-4 rounded-full"
|
||||
/>
|
||||
|
|
|
|||
BIN
public/apple-touch-icon-precomposed.png
Normal file
BIN
public/apple-touch-icon-precomposed.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 599 B |
|
|
@ -22,6 +22,6 @@ export const QuerySchema = z.object({
|
|||
limit: z.coerce.number().int().safe().default(listQueryLimit),
|
||||
})
|
||||
|
||||
export const FilterSchema = QuerySchema.omit({ id: true, startAt: true, endAt: true, limit: true }).extend({
|
||||
index1: z.string().optional(),
|
||||
})
|
||||
// export const FilterSchema = QuerySchema.omit({ id: true, startAt: true, endAt: true, limit: true }).extend({
|
||||
// index1: z.string().optional(),
|
||||
// })
|
||||
|
|
|
|||
|
|
@ -1,23 +1,25 @@
|
|||
import type { FilterSchema, QuerySchema } from '@/schemas/query'
|
||||
import type { QuerySchema } from '@/schemas/query'
|
||||
import type { SelectStatement } from 'sql-bricks'
|
||||
import type { z } from 'zod'
|
||||
|
||||
export type Query = z.infer<typeof QuerySchema>
|
||||
export type Filter = z.infer<typeof FilterSchema>
|
||||
const { in: $in, and, eq } = SqlBricks
|
||||
|
||||
export function query2filter(query: Query): Filter {
|
||||
const filter: Filter = {}
|
||||
export type Query = z.infer<typeof QuerySchema>
|
||||
|
||||
export function query2filter(query: Query) {
|
||||
const filter = []
|
||||
if (query.id)
|
||||
filter.index1 = query.id
|
||||
filter.push(eq('index1', query.id))
|
||||
|
||||
Object.keys(logsMap).forEach((key) => {
|
||||
// @ts-expect-error todo
|
||||
if (query[key]) {
|
||||
// @ts-expect-error todo
|
||||
filter[logsMap[key]] = query[key]
|
||||
filter.push($in(logsMap[key], query[key]))
|
||||
}
|
||||
})
|
||||
return filter
|
||||
console.log('query2filter', query, filter)
|
||||
return filter.length ? and(...filter) : []
|
||||
}
|
||||
|
||||
export function appendTimeFilter(sql: SelectStatement, query: Query): unknown {
|
||||
|
|
|
|||
|
|
@ -1,2 +1,7 @@
|
|||
// @ts-expect-error todo
|
||||
export { default as SqlBricks } from 'mysql-bricks'
|
||||
import type SqlBricks from 'sql-bricks'
|
||||
// @ts-expect-error use SqlBricks as a type
|
||||
import MySqlBricks from 'mysql-bricks'
|
||||
|
||||
const Bricks = MySqlBricks as unknown as typeof SqlBricks
|
||||
|
||||
export { Bricks as SqlBricks }
|
||||
|
|
|
|||
Loading…
Reference in a new issue