mirror of
https://github.com/SecurityBrewery/catalyst.git
synced 2025-12-07 15:52:47 +01:00
perf: search (#1091)
This commit is contained in:
11
.github/codecov.yml
vendored
Normal file
11
.github/codecov.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
threshold: 5%
|
||||
patch: off
|
||||
comment:
|
||||
layout: diff
|
||||
parsers:
|
||||
go:
|
||||
partials_as_hits: true
|
||||
49
migrations/7_search_view.go
Normal file
49
migrations/7_search_view.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
)
|
||||
|
||||
const searchViewName = "ticket_search"
|
||||
|
||||
const searchViewQuery = `
|
||||
SELECT
|
||||
tickets.id,
|
||||
tickets.name,
|
||||
tickets.created,
|
||||
tickets.description,
|
||||
tickets.open,
|
||||
tickets.type,
|
||||
tickets.state,
|
||||
users.name as owner_name,
|
||||
group_concat(comments.message) as comment_messages,
|
||||
group_concat(files.name) as file_names,
|
||||
group_concat(links.name) as link_names,
|
||||
group_concat(links.url) as link_urls,
|
||||
group_concat(tasks.name) as task_names,
|
||||
group_concat(timeline.message) as timeline_messages
|
||||
FROM tickets
|
||||
LEFT JOIN comments ON comments.ticket = tickets.id
|
||||
LEFT JOIN files ON files.ticket = tickets.id
|
||||
LEFT JOIN links ON links.ticket = tickets.id
|
||||
LEFT JOIN tasks ON tasks.ticket = tickets.id
|
||||
LEFT JOIN timeline ON timeline.ticket = tickets.id
|
||||
LEFT JOIN users ON users.id = tickets.owner
|
||||
GROUP BY tickets.id
|
||||
`
|
||||
|
||||
func searchViewUp(db dbx.Builder) error {
|
||||
return daos.New(db).SaveCollection(internalView(searchViewName, searchViewQuery))
|
||||
}
|
||||
|
||||
func searchViewDown(db dbx.Builder) error {
|
||||
dao := daos.New(db)
|
||||
|
||||
id, err := dao.FindCollectionByNameOrId(searchViewName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return dao.DeleteCollection(id)
|
||||
}
|
||||
@@ -11,4 +11,5 @@ func Register() {
|
||||
migrations.Register(viewsUp, viewsDown, "1700000004_views.go")
|
||||
migrations.Register(reactionsUp, reactionsDown, "1700000005_reactions.go")
|
||||
migrations.Register(systemuserUp, systemuserDown, "1700000006_systemuser.go")
|
||||
migrations.Register(searchViewUp, searchViewDown, "1700000007_search_view.go")
|
||||
}
|
||||
|
||||
@@ -17,9 +17,8 @@ import {
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
|
||||
import { Info, LoaderCircle, Search } from 'lucide-vue-next'
|
||||
import { LoaderCircle, Search } from 'lucide-vue-next'
|
||||
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import debounce from 'lodash.debounce'
|
||||
@@ -28,7 +27,7 @@ import { computed, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { pb } from '@/lib/pocketbase'
|
||||
import type { Ticket, Type } from '@/lib/types'
|
||||
import type { SearchTicket, Type } from '@/lib/types'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@@ -48,15 +47,13 @@ const filter = computed(() => {
|
||||
let raws: Array<string> = [
|
||||
'name ~ {:search}',
|
||||
'description ~ {:search}',
|
||||
'owner.name ~ {:search}',
|
||||
'owner.email ~ {:search}',
|
||||
'links_via_ticket.name ~ {:search}',
|
||||
'links_via_ticket.url ~ {:search}',
|
||||
'tasks_via_ticket.name ~ {:search}',
|
||||
'comments_via_ticket.message ~ {:search}',
|
||||
'files_via_ticket.name ~ {:search}',
|
||||
'timeline_via_ticket.message ~ {:search}',
|
||||
'state.severity ~ {:search}'
|
||||
'owner_name ~ {:search}',
|
||||
'comment_messages ~ {:search}',
|
||||
'file_names ~ {:search}',
|
||||
'link_names ~ {:search}',
|
||||
'link_urls ~ {:search}',
|
||||
'task_names ~ {:search}',
|
||||
'timeline_messages ~ {:search}'
|
||||
]
|
||||
|
||||
Object.keys(props.selectedType.schema.properties).forEach((key) => {
|
||||
@@ -96,12 +93,10 @@ const {
|
||||
refetch
|
||||
} = useQuery({
|
||||
queryKey: ['tickets', filter.value],
|
||||
queryFn: (): Promise<ListResult<Ticket>> =>
|
||||
pb.collection('tickets').getList(page.value, perPage.value, {
|
||||
queryFn: (): Promise<ListResult<SearchTicket>> =>
|
||||
pb.collection('ticket_search').getList(page.value, perPage.value, {
|
||||
sort: '-created',
|
||||
filter: filter.value,
|
||||
expand:
|
||||
'type,owner,comments_via_ticket.author,files_via_ticket,timeline_via_ticket,links_via_ticket,tasks_via_ticket.owner'
|
||||
filter: filter.value
|
||||
})
|
||||
})
|
||||
|
||||
@@ -139,9 +134,9 @@ watch([tab, props.selectedType, page, perPage], () => refetch())
|
||||
<Tabs v-model="tab" class="flex flex-1 flex-col overflow-hidden">
|
||||
<div class="flex items-center justify-between px-4 pt-2">
|
||||
<TabsList>
|
||||
<TabsTrigger value="all"> All</TabsTrigger>
|
||||
<TabsTrigger value="open"> Open</TabsTrigger>
|
||||
<TabsTrigger value="closed"> Closed</TabsTrigger>
|
||||
<TabsTrigger value="all">All</TabsTrigger>
|
||||
<TabsTrigger value="open">Open</TabsTrigger>
|
||||
<TabsTrigger value="closed">Closed</TabsTrigger>
|
||||
</TabsList>
|
||||
<!-- Button variant="outline" size="sm" class="h-7 gap-1 rounded-md px-3">
|
||||
<ListFilter class="h-3.5 w-3.5" />
|
||||
@@ -155,23 +150,6 @@ watch([tab, props.selectedType, page, perPage], () => refetch())
|
||||
<span class="absolute inset-y-0 start-0 flex items-center justify-center px-2">
|
||||
<Search class="size-4 text-muted-foreground" />
|
||||
</span>
|
||||
|
||||
<div>
|
||||
<TooltipProvider :delay-duration="0">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Info class="ml-2 size-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p class="w-64">
|
||||
Search name, description, or owner. Links, tasks, comments, files, and
|
||||
timeline messages are also searched, but cause unreliable results if there are
|
||||
more than 1000 records.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -222,7 +200,6 @@ watch([tab, props.selectedType, page, perPage], () => refetch())
|
||||
</PaginationListItem>
|
||||
<PaginationEllipsis v-else :key="item.type" :index="index" />
|
||||
</template>
|
||||
|
||||
<PaginationNext />
|
||||
<PaginationLast />
|
||||
</PaginationList>
|
||||
|
||||
@@ -3,12 +3,12 @@ import ResourceListElement from '@/components/common/ResourceListElement.vue'
|
||||
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import type { Ticket } from '@/lib/types'
|
||||
import type { SearchTicket } from '@/lib/types'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
defineProps<{
|
||||
tickets: Array<Ticket>
|
||||
tickets: Array<SearchTicket>
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -19,10 +19,10 @@ defineProps<{
|
||||
:key="item.id"
|
||||
:title="item.name"
|
||||
:created="item.created"
|
||||
:subtitle="item.expand.owner ? item.expand.owner.name : ''"
|
||||
:subtitle="item.owner_name"
|
||||
:description="item.description ? item.description.substring(0, 300) : ''"
|
||||
:active="route.params.id === item.id"
|
||||
:to="`/tickets/${item.expand.type.id}/${item.id}`"
|
||||
:to="`/tickets/${item.type}/${item.id}`"
|
||||
:open="item.open"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -32,6 +32,16 @@ export interface Ticket {
|
||||
}
|
||||
}
|
||||
|
||||
export interface SearchTicket {
|
||||
id: string
|
||||
name: string
|
||||
created: string
|
||||
description: string
|
||||
open: boolean
|
||||
type: string
|
||||
owner_name: string
|
||||
}
|
||||
|
||||
export interface Task {
|
||||
id: string
|
||||
|
||||
|
||||
Reference in New Issue
Block a user