mirror of
https://github.com/SecurityBrewery/catalyst.git
synced 2025-12-08 16:22:46 +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(viewsUp, viewsDown, "1700000004_views.go")
|
||||||
migrations.Register(reactionsUp, reactionsDown, "1700000005_reactions.go")
|
migrations.Register(reactionsUp, reactionsDown, "1700000005_reactions.go")
|
||||||
migrations.Register(systemuserUp, systemuserDown, "1700000006_systemuser.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 { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
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 { useQuery } from '@tanstack/vue-query'
|
||||||
import debounce from 'lodash.debounce'
|
import debounce from 'lodash.debounce'
|
||||||
@@ -28,7 +27,7 @@ import { computed, ref, watch } from 'vue'
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
import { pb } from '@/lib/pocketbase'
|
import { pb } from '@/lib/pocketbase'
|
||||||
import type { Ticket, Type } from '@/lib/types'
|
import type { SearchTicket, Type } from '@/lib/types'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -48,15 +47,13 @@ const filter = computed(() => {
|
|||||||
let raws: Array<string> = [
|
let raws: Array<string> = [
|
||||||
'name ~ {:search}',
|
'name ~ {:search}',
|
||||||
'description ~ {:search}',
|
'description ~ {:search}',
|
||||||
'owner.name ~ {:search}',
|
'owner_name ~ {:search}',
|
||||||
'owner.email ~ {:search}',
|
'comment_messages ~ {:search}',
|
||||||
'links_via_ticket.name ~ {:search}',
|
'file_names ~ {:search}',
|
||||||
'links_via_ticket.url ~ {:search}',
|
'link_names ~ {:search}',
|
||||||
'tasks_via_ticket.name ~ {:search}',
|
'link_urls ~ {:search}',
|
||||||
'comments_via_ticket.message ~ {:search}',
|
'task_names ~ {:search}',
|
||||||
'files_via_ticket.name ~ {:search}',
|
'timeline_messages ~ {:search}'
|
||||||
'timeline_via_ticket.message ~ {:search}',
|
|
||||||
'state.severity ~ {:search}'
|
|
||||||
]
|
]
|
||||||
|
|
||||||
Object.keys(props.selectedType.schema.properties).forEach((key) => {
|
Object.keys(props.selectedType.schema.properties).forEach((key) => {
|
||||||
@@ -96,12 +93,10 @@ const {
|
|||||||
refetch
|
refetch
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ['tickets', filter.value],
|
queryKey: ['tickets', filter.value],
|
||||||
queryFn: (): Promise<ListResult<Ticket>> =>
|
queryFn: (): Promise<ListResult<SearchTicket>> =>
|
||||||
pb.collection('tickets').getList(page.value, perPage.value, {
|
pb.collection('ticket_search').getList(page.value, perPage.value, {
|
||||||
sort: '-created',
|
sort: '-created',
|
||||||
filter: filter.value,
|
filter: filter.value
|
||||||
expand:
|
|
||||||
'type,owner,comments_via_ticket.author,files_via_ticket,timeline_via_ticket,links_via_ticket,tasks_via_ticket.owner'
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -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">
|
<span class="absolute inset-y-0 start-0 flex items-center justify-center px-2">
|
||||||
<Search class="size-4 text-muted-foreground" />
|
<Search class="size-4 text-muted-foreground" />
|
||||||
</span>
|
</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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -222,7 +200,6 @@ watch([tab, props.selectedType, page, perPage], () => refetch())
|
|||||||
</PaginationListItem>
|
</PaginationListItem>
|
||||||
<PaginationEllipsis v-else :key="item.type" :index="index" />
|
<PaginationEllipsis v-else :key="item.type" :index="index" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<PaginationNext />
|
<PaginationNext />
|
||||||
<PaginationLast />
|
<PaginationLast />
|
||||||
</PaginationList>
|
</PaginationList>
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import ResourceListElement from '@/components/common/ResourceListElement.vue'
|
|||||||
|
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
import type { Ticket } from '@/lib/types'
|
import type { SearchTicket } from '@/lib/types'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
tickets: Array<Ticket>
|
tickets: Array<SearchTicket>
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -19,10 +19,10 @@ defineProps<{
|
|||||||
:key="item.id"
|
:key="item.id"
|
||||||
:title="item.name"
|
:title="item.name"
|
||||||
:created="item.created"
|
:created="item.created"
|
||||||
:subtitle="item.expand.owner ? item.expand.owner.name : ''"
|
:subtitle="item.owner_name"
|
||||||
:description="item.description ? item.description.substring(0, 300) : ''"
|
:description="item.description ? item.description.substring(0, 300) : ''"
|
||||||
:active="route.params.id === item.id"
|
:active="route.params.id === item.id"
|
||||||
:to="`/tickets/${item.expand.type.id}/${item.id}`"
|
:to="`/tickets/${item.type}/${item.id}`"
|
||||||
:open="item.open"
|
:open="item.open"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 {
|
export interface Task {
|
||||||
id: string
|
id: string
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user