fix: events list view (#878)

* fix: filter by event id

* fix: run count

* feat: filter by id api

* feat: filter by Event Id

* chore: default page is runs

* feat: cancel event runs

---------

Co-authored-by: Alexander Belanger <alexander@hatchet.run>
This commit is contained in:
Gabe Ruttner
2024-09-16 09:46:31 -07:00
committed by GitHub
parent c64c62f66a
commit af9ed49f1e
22 changed files with 990 additions and 305 deletions
@@ -98,6 +98,8 @@ EventList:
$ref: "./event.yaml#/EventList"
ReplayEventRequest:
$ref: "./event.yaml#/ReplayEventRequest"
CancelEventRequest:
$ref: "./event.yaml#/CancelEventRequest"
Workflow:
$ref: "./workflow.yaml#/Workflow"
WorkflowConcurrency:
@@ -58,6 +58,19 @@ ReplayEventRequest:
required:
- eventIds
CancelEventRequest:
properties:
eventIds:
type: array
items:
type: string
example: bb214807-246e-43a5-a25d-41761d1cff9e
minLength: 36
maxLength: 36
format: uuid
required:
- eventIds
EventWorkflowRunSummary:
properties:
pending:
+2
View File
@@ -96,6 +96,8 @@ paths:
$ref: "./paths/event/event.yaml#/withTenant"
/api/v1/tenants/{tenant}/events/replay:
$ref: "./paths/event/event.yaml#/replayEvents"
/api/v1/tenants/{tenant}/events/cancel:
$ref: "./paths/event/event.yaml#/cancelEvents"
/api/v1/tenants/{tenant}/members:
$ref: "./paths/tenant/tenant.yaml#/members"
/api/v1/tenants/{tenant}/members/{member}:
@@ -76,6 +76,17 @@ withTenant:
type: array
items:
type: string
- description: A list of event ids to filter by
in: query
name: eventIds
required: false
schema:
type: array
items:
type: string
format: uuid
minLength: 36
maxLength: 36
responses:
"200":
content:
@@ -312,3 +323,62 @@ replayEvents:
summary: Replay events
tags:
- Event
cancelEvents:
post:
x-resources: ["tenant"]
description: Cancels all runs for a list of events.
operationId: event:update:cancel
parameters:
- description: The tenant id
in: path
name: tenant
required: true
schema:
type: string
format: uuid
minLength: 36
maxLength: 36
requestBody:
content:
application/json:
schema:
$ref: "../../components/schemas/_index.yaml#/CancelEventRequest"
description: The event ids to replay
required: true
responses:
"200":
content:
application/json:
schema:
type: object
properties:
workflowRunIds:
type: array
items:
type: string
format: uuid
minLength: 36
maxLength: 36
description: Successfully canceled runs for the events
"400":
content:
application/json:
schema:
$ref: "../../components/schemas/_index.yaml#/APIErrors"
description: A malformed or bad request
"403":
content:
application/json:
schema:
$ref: "../../components/schemas/_index.yaml#/APIErrors"
description: Forbidden
"429":
content:
application/json:
schema:
$ref: "../../components/schemas/_index.yaml#/APIErrors"
description: Resource limit exceeded
summary: Replay events
tags:
- Event
+100
View File
@@ -0,0 +1,100 @@
package events
import (
"fmt"
"sync"
"github.com/google/uuid"
"github.com/hashicorp/go-multierror"
"github.com/labstack/echo/v4"
"github.com/hatchet-dev/hatchet/api/v1/server/oas/gen"
"github.com/hatchet-dev/hatchet/internal/msgqueue"
"github.com/hatchet-dev/hatchet/internal/services/shared/tasktypes"
"github.com/hatchet-dev/hatchet/pkg/repository"
"github.com/hatchet-dev/hatchet/pkg/repository/prisma/db"
"github.com/hatchet-dev/hatchet/pkg/repository/prisma/sqlchelpers"
)
func (t *EventService) EventUpdateCancel(ctx echo.Context, request gen.EventUpdateCancelRequestObject) (gen.EventUpdateCancelResponseObject, error) {
tenant := ctx.Get("tenant").(*db.TenantModel)
eventIds := make([]string, len(request.Body.EventIds))
runIds := make([]string, 0)
for i := range request.Body.EventIds {
eventIds[i] = request.Body.EventIds[i].String()
}
for i := range eventIds {
eventId := eventIds[i]
runs, err := t.config.EngineRepository.WorkflowRun().ListWorkflowRuns(ctx.Request().Context(), tenant.ID, &repository.ListWorkflowRunsOpts{
EventId: &eventId,
})
if err != nil {
return nil, err
}
for _, run := range runs.Rows {
runCp := run
runIds = append(runIds, sqlchelpers.UUIDToStr(runCp.WorkflowRun.ID))
}
}
var wg sync.WaitGroup
var mu sync.Mutex
var cancelledWorkflowRunIds []uuid.UUID
var returnErr error
for _, runId := range runIds {
runIdCp := runId
wg.Add(1)
go func(runId string) {
defer wg.Done()
// Lookup step runs for the workflow run
jobRun, err := t.config.EngineRepository.JobRun().ListJobRunsForWorkflowRun(ctx.Request().Context(), tenant.ID, runId)
if err != nil {
returnErr = multierror.Append(err, fmt.Errorf("failed to list job runs for workflow run %s", runId))
return
}
for _, jobRun := range jobRun {
// If the step run is not in a final state, send a task to the taskqueue to cancel it
var reason = "CANCELLED_BY_USER"
// send a task to the taskqueue
jobRunId := sqlchelpers.UUIDToStr(jobRun.ID)
err = t.config.MessageQueue.AddMessage(
ctx.Request().Context(),
msgqueue.JOB_PROCESSING_QUEUE,
tasktypes.JobRunCancelledToTask(tenant.ID, jobRunId, &reason),
)
if err != nil {
returnErr = multierror.Append(err, fmt.Errorf("failed to send cancel task for job run %s", jobRunId))
continue
}
}
// Add the canceled workflow run ID to the slice
mu.Lock()
cancelledWorkflowRunIds = append(cancelledWorkflowRunIds, uuid.MustParse(runId))
mu.Unlock()
}(runIdCp)
}
wg.Wait()
if returnErr != nil {
return nil, returnErr
}
// Create a new instance of gen.WorkflowRunCancel200JSONResponse and assign canceledWorkflowRunUUIDs to its WorkflowRunIds field
response := gen.EventUpdateCancel200JSONResponse{
WorkflowRunIds: &cancelledWorkflowRunIds,
}
return response, nil
}
+11
View File
@@ -91,6 +91,17 @@ func (t *EventService) EventList(ctx echo.Context, request gen.EventListRequestO
listOpts.AdditionalMetadata = additionalMetadataBytes
}
if request.Params.EventIds != nil {
eventIds := make([]string, len(*request.Params.EventIds))
for i, id := range *request.Params.EventIds {
idCp := id
eventIds[i] = idCp.String()
}
listOpts.Ids = eventIds
}
dbCtx, cancel := context.WithTimeout(ctx.Request().Context(), 30*time.Second)
defer cancel()
+292 -170
View File
@@ -262,6 +262,11 @@ type AcceptInviteRequest struct {
Invite string `json:"invite" validate:"required,uuid"`
}
// CancelEventRequest defines model for CancelEventRequest.
type CancelEventRequest struct {
EventIds []openapi_types.UUID `json:"eventIds"`
}
// CreateAPITokenRequest defines model for CreateAPITokenRequest.
type CreateAPITokenRequest struct {
// ExpiresIn The duration for which the token is valid.
@@ -1322,6 +1327,9 @@ type EventListParams struct {
// AdditionalMetadata A list of metadata key value pairs to filter by
AdditionalMetadata *[]string `form:"additionalMetadata,omitempty" json:"additionalMetadata,omitempty"`
// EventIds A list of event ids to filter by
EventIds *[]openapi_types.UUID `form:"eventIds,omitempty" json:"eventIds,omitempty"`
}
// TenantGetQueueMetricsParams defines parameters for TenantGetQueueMetrics.
@@ -1444,6 +1452,9 @@ type ApiTokenCreateJSONRequestBody = CreateAPITokenRequest
// EventCreateJSONRequestBody defines body for EventCreate for application/json ContentType.
type EventCreateJSONRequestBody = CreateEventRequest
// EventUpdateCancelJSONRequestBody defines body for EventUpdateCancel for application/json ContentType.
type EventUpdateCancelJSONRequestBody = CancelEventRequest
// EventUpdateReplayJSONRequestBody defines body for EventUpdateReplay for application/json ContentType.
type EventUpdateReplayJSONRequestBody = ReplayEventRequest
@@ -1566,6 +1577,9 @@ type ServerInterface interface {
// Create event
// (POST /api/v1/tenants/{tenant}/events)
EventCreate(ctx echo.Context, tenant openapi_types.UUID) error
// Replay events
// (POST /api/v1/tenants/{tenant}/events/cancel)
EventUpdateCancel(ctx echo.Context, tenant openapi_types.UUID) error
// List event keys
// (GET /api/v1/tenants/{tenant}/events/keys)
EventKeyList(ctx echo.Context, tenant openapi_types.UUID) error
@@ -2294,6 +2308,13 @@ func (w *ServerInterfaceWrapper) EventList(ctx echo.Context) error {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter additionalMetadata: %s", err))
}
// ------------- Optional query parameter "eventIds" -------------
err = runtime.BindQueryParameter("form", true, false, "eventIds", ctx.QueryParams(), &params.EventIds)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter eventIds: %s", err))
}
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.EventList(ctx, tenant, params)
return err
@@ -2319,6 +2340,26 @@ func (w *ServerInterfaceWrapper) EventCreate(ctx echo.Context) error {
return err
}
// EventUpdateCancel converts echo context to params.
func (w *ServerInterfaceWrapper) EventUpdateCancel(ctx echo.Context) error {
var err error
// ------------- Path parameter "tenant" -------------
var tenant openapi_types.UUID
err = runtime.BindStyledParameterWithLocation("simple", false, "tenant", runtime.ParamLocationPath, ctx.Param("tenant"), &tenant)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter tenant: %s", err))
}
ctx.Set(BearerAuthScopes, []string{})
ctx.Set(CookieAuthScopes, []string{})
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.EventUpdateCancel(ctx, tenant)
return err
}
// EventKeyList converts echo context to params.
func (w *ServerInterfaceWrapper) EventKeyList(ctx echo.Context) error {
var err error
@@ -3587,6 +3628,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
router.POST(baseURL+"/api/v1/tenants/:tenant/api-tokens", wrapper.ApiTokenCreate)
router.GET(baseURL+"/api/v1/tenants/:tenant/events", wrapper.EventList)
router.POST(baseURL+"/api/v1/tenants/:tenant/events", wrapper.EventCreate)
router.POST(baseURL+"/api/v1/tenants/:tenant/events/cancel", wrapper.EventUpdateCancel)
router.GET(baseURL+"/api/v1/tenants/:tenant/events/keys", wrapper.EventKeyList)
router.POST(baseURL+"/api/v1/tenants/:tenant/events/replay", wrapper.EventUpdateReplay)
router.GET(baseURL+"/api/v1/tenants/:tenant/invites", wrapper.TenantInviteList)
@@ -4523,6 +4565,53 @@ func (response EventCreate429JSONResponse) VisitEventCreateResponse(w http.Respo
return json.NewEncoder(w).Encode(response)
}
type EventUpdateCancelRequestObject struct {
Tenant openapi_types.UUID `json:"tenant"`
Body *EventUpdateCancelJSONRequestBody
}
type EventUpdateCancelResponseObject interface {
VisitEventUpdateCancelResponse(w http.ResponseWriter) error
}
type EventUpdateCancel200JSONResponse struct {
WorkflowRunIds *[]openapi_types.UUID `json:"workflowRunIds,omitempty"`
}
func (response EventUpdateCancel200JSONResponse) VisitEventUpdateCancelResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
return json.NewEncoder(w).Encode(response)
}
type EventUpdateCancel400JSONResponse APIErrors
func (response EventUpdateCancel400JSONResponse) VisitEventUpdateCancelResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(400)
return json.NewEncoder(w).Encode(response)
}
type EventUpdateCancel403JSONResponse APIErrors
func (response EventUpdateCancel403JSONResponse) VisitEventUpdateCancelResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(403)
return json.NewEncoder(w).Encode(response)
}
type EventUpdateCancel429JSONResponse APIErrors
func (response EventUpdateCancel429JSONResponse) VisitEventUpdateCancelResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(429)
return json.NewEncoder(w).Encode(response)
}
type EventKeyListRequestObject struct {
Tenant openapi_types.UUID `json:"tenant"`
}
@@ -6584,6 +6673,8 @@ type StrictServerInterface interface {
EventCreate(ctx echo.Context, request EventCreateRequestObject) (EventCreateResponseObject, error)
EventUpdateCancel(ctx echo.Context, request EventUpdateCancelRequestObject) (EventUpdateCancelResponseObject, error)
EventKeyList(ctx echo.Context, request EventKeyListRequestObject) (EventKeyListResponseObject, error)
EventUpdateReplay(ctx echo.Context, request EventUpdateReplayRequestObject) (EventUpdateReplayResponseObject, error)
@@ -7356,6 +7447,37 @@ func (sh *strictHandler) EventCreate(ctx echo.Context, tenant openapi_types.UUID
return nil
}
// EventUpdateCancel operation middleware
func (sh *strictHandler) EventUpdateCancel(ctx echo.Context, tenant openapi_types.UUID) error {
var request EventUpdateCancelRequestObject
request.Tenant = tenant
var body EventUpdateCancelJSONRequestBody
if err := ctx.Bind(&body); err != nil {
return err
}
request.Body = &body
handler := func(ctx echo.Context, request interface{}) (interface{}, error) {
return sh.ssi.EventUpdateCancel(ctx, request.(EventUpdateCancelRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "EventUpdateCancel")
}
response, err := handler(ctx, request)
if err != nil {
return err
} else if validResponse, ok := response.(EventUpdateCancelResponseObject); ok {
return validResponse.VisitEventUpdateCancelResponse(ctx.Response())
} else if response != nil {
return fmt.Errorf("Unexpected response type: %T", response)
}
return nil
}
// EventKeyList operation middleware
func (sh *strictHandler) EventKeyList(ctx echo.Context, tenant openapi_types.UUID) error {
var request EventKeyListRequestObject
@@ -8764,176 +8886,176 @@ func (sh *strictHandler) WorkflowVersionGet(ctx echo.Context, workflow openapi_t
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
"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",
"X/5/AAAA///Wr4s2DaIBAA==",
"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",
}
// GetSwagger returns the content of the embedded swagger specification file
@@ -43,7 +43,12 @@ const keyValuePairSchema = z.object({
value: z.string().min(1, 'Value is required'),
});
const arrayInputSchema = z.object({
values: z.string().min(1, 'At least one value is required'),
});
type KeyValuePair = z.infer<typeof keyValuePairSchema>;
type ArrayInput = z.infer<typeof arrayInputSchema>;
export function DataTableFacetedFilter<TData, TValue>({
column,
@@ -53,13 +58,22 @@ export function DataTableFacetedFilter<TData, TValue>({
}: DataTableFacetedFilterProps<TData, TValue>) {
const selectedValues = new Set(column?.getFilterValue() as string[]);
const { register, handleSubmit, reset } = useForm<KeyValuePair>({
resolver: zodResolver(keyValuePairSchema),
defaultValues: { key: '', value: '' },
const { register, handleSubmit, reset } = useForm<KeyValuePair | ArrayInput>({
resolver: zodResolver(
type === ToolbarType.KeyValue ? keyValuePairSchema : arrayInputSchema,
),
defaultValues:
type === ToolbarType.KeyValue ? { key: '', value: '' } : { values: '' },
});
const onSubmit = (data: KeyValuePair) => {
selectedValues.add(`${data.key}:${data.value}`);
const onSubmit = (data: KeyValuePair | ArrayInput) => {
if ('key' in data) {
selectedValues.add(`${data.key}:${data.value}`);
} else {
data.values
.split(',')
.forEach((value) => selectedValues.add(value.trim()));
}
const filterValues = Array.from(selectedValues);
column?.setFilterValue(filterValues.length ? filterValues : undefined);
reset();
@@ -94,7 +108,7 @@ export function DataTableFacetedFilter<TData, TValue>({
>
{selectedValues.size} selected
</Badge>
) : type === ToolbarType.KeyValue ? (
) : (
Array.from(selectedValues).map((option, index) => (
<Badge
key={index}
@@ -112,26 +126,6 @@ export function DataTableFacetedFilter<TData, TValue>({
</Button>
</Badge>
))
) : (
options
?.filter((option) => selectedValues.has(option.value))
.map((option) => (
<Badge
key={option.value}
variant="secondary"
className="rounded-sm px-1 font-normal flex items-center space-x-1"
>
{option.label}
<Button
variant="ghost"
size="xs"
className="ml-2"
onClick={() => handleRemove(option.value)}
>
<BiX className="h-3 w-3" />
</Button>
</Badge>
))
)}
</div>
</>
@@ -139,7 +133,7 @@ export function DataTableFacetedFilter<TData, TValue>({
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-2" align="start">
{type === ToolbarType.KeyValue && (
{[ToolbarType.Array, ToolbarType.KeyValue].includes(type) && (
<div>
<div className="">
{Array.from(selectedValues).map((filter, index) => (
@@ -161,34 +155,43 @@ export function DataTableFacetedFilter<TData, TValue>({
))}
</div>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex items-center space-x-2 mb-2">
<Input
type="text"
placeholder="Key"
{...register('key')}
className="flex-1"
/>
<Input
type="text"
placeholder="Value"
{...register('value')}
className="flex-1"
/>
</div>
{type === ToolbarType.KeyValue ? (
<div className="flex items-center space-x-2 mb-2">
<Input
type="text"
placeholder="Key"
{...register('key')}
className="flex-1"
/>
<Input
type="text"
placeholder="Value"
{...register('value')}
className="flex-1"
/>
</div>
) : (
<div className="mb-2">
<Input
type="text"
placeholder="Enter values (comma-separated)"
{...register('values')}
className="w-full"
/>
</div>
)}
<Button type="submit" className="w-full" size="sm">
Add {title} Filter
</Button>
{selectedValues.size > 0 && (
<>
<Button
onClick={() => column?.setFilterValue(undefined)}
className="w-full mt-2"
size="sm"
variant={'ghost'}
>
Reset
</Button>
</>
<Button
onClick={() => column?.setFilterValue(undefined)}
className="w-full mt-2"
size="sm"
variant={'ghost'}
>
Reset
</Button>
)}
</form>
</div>
@@ -17,6 +17,7 @@ export enum ToolbarType {
Checkbox = 'checkbox',
Radio = 'radio',
KeyValue = 'key-value',
Array = 'array',
}
export type ToolbarFilters = {
+27
View File
@@ -14,6 +14,7 @@ import {
APIError,
APIErrors,
APIMeta,
CancelEventRequest,
CreateAPITokenRequest,
CreateAPITokenResponse,
CreateEventRequest,
@@ -854,6 +855,8 @@ export class Api<SecurityDataType = unknown> extends HttpClient<SecurityDataType
* @example ["key1:value1","key2:value2"]
*/
additionalMetadata?: string[];
/** A list of event ids to filter by */
eventIds?: string[];
},
params: RequestParams = {},
) =>
@@ -903,6 +906,30 @@ export class Api<SecurityDataType = unknown> extends HttpClient<SecurityDataType
format: 'json',
...params,
});
/**
* @description Cancels all runs for a list of events.
*
* @tags Event
* @name EventUpdateCancel
* @summary Replay events
* @request POST:/api/v1/tenants/{tenant}/events/cancel
* @secure
*/
eventUpdateCancel = (tenant: string, data: CancelEventRequest, params: RequestParams = {}) =>
this.request<
{
workflowRunIds?: string[];
},
APIErrors
>({
path: `/api/v1/tenants/${tenant}/events/cancel`,
method: 'POST',
body: data,
secure: true,
type: ContentType.Json,
format: 'json',
...params,
});
/**
* @description Gets a list of tenant members
*
@@ -493,6 +493,10 @@ export interface ReplayEventRequest {
eventIds: string[];
}
export interface CancelEventRequest {
eventIds: string[];
}
export interface Workflow {
metadata: APIResourceMeta;
/** The name of the workflow. */
@@ -49,6 +49,17 @@ export const columns = ({
enableSorting: false,
enableHiding: false,
},
{
accessorKey: 'EventId',
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Event Id" />
),
cell: ({ row }) => (
<div className="w-full">{row.original.metadata.id}</div>
),
enableSorting: false,
enableHiding: true,
},
{
accessorKey: 'key',
header: ({ column }) => (
+46 -1
View File
@@ -45,6 +45,7 @@ import { Loading } from '@/components/ui/loading.tsx';
import { TenantContextType } from '@/lib/outlet';
import RelativeDate from '@/components/molecules/relative-date';
import { CreateEventForm } from './components/create-event-form';
import { BiX } from 'react-icons/bi';
export default function Events() {
return (
@@ -116,7 +117,9 @@ function EventsTable() {
}
return [];
});
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({
EventId: false,
});
const [pagination, setPagination] = useState<PaginationState>(() => {
const pageIndex = Number(searchParams.get('pageIndex')) || 0;
@@ -204,6 +207,16 @@ function EventsTable() {
return filter?.value as Array<WorkflowRunStatus>;
}, [columnFilters]);
const eventIds = useMemo(() => {
const filter = columnFilters.find((filter) => filter.id === 'EventId');
if (!filter) {
return;
}
return filter?.value as Array<string>;
}, [columnFilters]);
const AdditionalMetadataFilter = useMemo(() => {
const filter = columnFilters.find((filter) => filter.id === 'Metadata');
@@ -238,10 +251,22 @@ function EventsTable() {
search,
statuses,
additionalMetadata: AdditionalMetadataFilter,
eventIds: eventIds,
}),
refetchInterval: 2000,
});
const cancelEventsMutation = useMutation({
mutationKey: ['event:update:cancel', tenant.metadata.id],
mutationFn: async (data: ReplayEventRequest) => {
await api.eventUpdateCancel(tenant.metadata.id, data);
},
onSuccess: () => {
refetch();
},
onError: handleApiError,
});
const replayEventsMutation = useMutation({
mutationKey: ['event:update:replay', tenant.metadata.id],
mutationFn: async (data: ReplayEventRequest) => {
@@ -333,6 +358,21 @@ function EventsTable() {
});
const actions = [
<Button
key="cancel"
disabled={Object.keys(rowSelection).length === 0}
variant={Object.keys(rowSelection).length === 0 ? 'outline' : 'default'}
size="sm"
className="h-8 px-2 lg:px-3 gap-2"
onClick={() => {
cancelEventsMutation.mutate({
eventIds: Object.keys(rowSelection),
});
}}
>
<BiX className="h-4 w-4" />
Cancel
</Button>,
<Button
key="replay"
disabled={Object.keys(rowSelection).length === 0}
@@ -431,6 +471,11 @@ function EventsTable() {
title: 'Metadata',
type: ToolbarType.KeyValue,
},
{
columnId: 'EventId',
title: 'Event Id',
type: ToolbarType.Array,
},
]}
showColumnToggle={true}
columnVisibility={columnVisibility}
+6 -6
View File
@@ -120,16 +120,16 @@ function Sidebar({ className, memberships, currTenant }: SidebarProps) {
<SidebarButtonPrimary
key={1}
onNavLinkClick={onNavLinkClick}
to="/events"
name="Events"
icon={<QueueListIcon className="mr-2 h-4 w-4" />}
to="/workflow-runs"
name="Workflow Runs"
icon={<AdjustmentsHorizontalIcon className="mr-2 h-4 w-4" />}
/>
<SidebarButtonPrimary
key={2}
onNavLinkClick={onNavLinkClick}
to="/workflow-runs"
name="Workflow Runs"
icon={<AdjustmentsHorizontalIcon className="mr-2 h-4 w-4" />}
to="/events"
name="Events"
icon={<QueueListIcon className="mr-2 h-4 w-4" />}
/>
</div>
</div>
+1 -1
View File
@@ -72,7 +72,7 @@ const routes: RouteObject[] = [
lazy: async () => {
return {
loader: function () {
return redirect('/events');
return redirect('/workflow-runs');
},
};
},
+201
View File
@@ -259,6 +259,11 @@ type AcceptInviteRequest struct {
Invite string `json:"invite" validate:"required,uuid"`
}
// CancelEventRequest defines model for CancelEventRequest.
type CancelEventRequest struct {
EventIds []openapi_types.UUID `json:"eventIds"`
}
// CreateAPITokenRequest defines model for CreateAPITokenRequest.
type CreateAPITokenRequest struct {
// ExpiresIn The duration for which the token is valid.
@@ -1319,6 +1324,9 @@ type EventListParams struct {
// AdditionalMetadata A list of metadata key value pairs to filter by
AdditionalMetadata *[]string `form:"additionalMetadata,omitempty" json:"additionalMetadata,omitempty"`
// EventIds A list of event ids to filter by
EventIds *[]openapi_types.UUID `form:"eventIds,omitempty" json:"eventIds,omitempty"`
}
// TenantGetQueueMetricsParams defines parameters for TenantGetQueueMetrics.
@@ -1441,6 +1449,9 @@ type ApiTokenCreateJSONRequestBody = CreateAPITokenRequest
// EventCreateJSONRequestBody defines body for EventCreate for application/json ContentType.
type EventCreateJSONRequestBody = CreateEventRequest
// EventUpdateCancelJSONRequestBody defines body for EventUpdateCancel for application/json ContentType.
type EventUpdateCancelJSONRequestBody = CancelEventRequest
// EventUpdateReplayJSONRequestBody defines body for EventUpdateReplay for application/json ContentType.
type EventUpdateReplayJSONRequestBody = ReplayEventRequest
@@ -1646,6 +1657,11 @@ type ClientInterface interface {
EventCreate(ctx context.Context, tenant openapi_types.UUID, body EventCreateJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error)
// EventUpdateCancelWithBody request with any body
EventUpdateCancelWithBody(ctx context.Context, tenant openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error)
EventUpdateCancel(ctx context.Context, tenant openapi_types.UUID, body EventUpdateCancelJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error)
// EventKeyList request
EventKeyList(ctx context.Context, tenant openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error)
@@ -2208,6 +2224,30 @@ func (c *Client) EventCreate(ctx context.Context, tenant openapi_types.UUID, bod
return c.Client.Do(req)
}
func (c *Client) EventUpdateCancelWithBody(ctx context.Context, tenant openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) {
req, err := NewEventUpdateCancelRequestWithBody(c.Server, tenant, contentType, body)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
return nil, err
}
return c.Client.Do(req)
}
func (c *Client) EventUpdateCancel(ctx context.Context, tenant openapi_types.UUID, body EventUpdateCancelJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) {
req, err := NewEventUpdateCancelRequest(c.Server, tenant, body)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
return nil, err
}
return c.Client.Do(req)
}
func (c *Client) EventKeyList(ctx context.Context, tenant openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) {
req, err := NewEventKeyListRequest(c.Server, tenant)
if err != nil {
@@ -4187,6 +4227,22 @@ func NewEventListRequest(server string, tenant openapi_types.UUID, params *Event
}
if params.EventIds != nil {
if queryFrag, err := runtime.StyleParamWithLocation("form", true, "eventIds", runtime.ParamLocationQuery, *params.EventIds); err != nil {
return nil, err
} else if parsed, err := url.ParseQuery(queryFrag); err != nil {
return nil, err
} else {
for k, v := range parsed {
for _, v2 := range v {
queryValues.Add(k, v2)
}
}
}
}
queryURL.RawQuery = queryValues.Encode()
}
@@ -4245,6 +4301,53 @@ func NewEventCreateRequestWithBody(server string, tenant openapi_types.UUID, con
return req, nil
}
// NewEventUpdateCancelRequest calls the generic EventUpdateCancel builder with application/json body
func NewEventUpdateCancelRequest(server string, tenant openapi_types.UUID, body EventUpdateCancelJSONRequestBody) (*http.Request, error) {
var bodyReader io.Reader
buf, err := json.Marshal(body)
if err != nil {
return nil, err
}
bodyReader = bytes.NewReader(buf)
return NewEventUpdateCancelRequestWithBody(server, tenant, "application/json", bodyReader)
}
// NewEventUpdateCancelRequestWithBody generates requests for EventUpdateCancel with any type of body
func NewEventUpdateCancelRequestWithBody(server string, tenant openapi_types.UUID, contentType string, body io.Reader) (*http.Request, error) {
var err error
var pathParam0 string
pathParam0, err = runtime.StyleParamWithLocation("simple", false, "tenant", runtime.ParamLocationPath, tenant)
if err != nil {
return nil, err
}
serverURL, err := url.Parse(server)
if err != nil {
return nil, err
}
operationPath := fmt.Sprintf("/api/v1/tenants/%s/events/cancel", pathParam0)
if operationPath[0] == '/' {
operationPath = "." + operationPath
}
queryURL, err := serverURL.Parse(operationPath)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", queryURL.String(), body)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", contentType)
return req, nil
}
// NewEventKeyListRequest generates requests for EventKeyList
func NewEventKeyListRequest(server string, tenant openapi_types.UUID) (*http.Request, error) {
var err error
@@ -6825,6 +6928,11 @@ type ClientWithResponsesInterface interface {
EventCreateWithResponse(ctx context.Context, tenant openapi_types.UUID, body EventCreateJSONRequestBody, reqEditors ...RequestEditorFn) (*EventCreateResponse, error)
// EventUpdateCancelWithBodyWithResponse request with any body
EventUpdateCancelWithBodyWithResponse(ctx context.Context, tenant openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*EventUpdateCancelResponse, error)
EventUpdateCancelWithResponse(ctx context.Context, tenant openapi_types.UUID, body EventUpdateCancelJSONRequestBody, reqEditors ...RequestEditorFn) (*EventUpdateCancelResponse, error)
// EventKeyListWithResponse request
EventKeyListWithResponse(ctx context.Context, tenant openapi_types.UUID, reqEditors ...RequestEditorFn) (*EventKeyListResponse, error)
@@ -7607,6 +7715,33 @@ func (r EventCreateResponse) StatusCode() int {
return 0
}
type EventUpdateCancelResponse struct {
Body []byte
HTTPResponse *http.Response
JSON200 *struct {
WorkflowRunIds *[]openapi_types.UUID `json:"workflowRunIds,omitempty"`
}
JSON400 *APIErrors
JSON403 *APIErrors
JSON429 *APIErrors
}
// Status returns HTTPResponse.Status
func (r EventUpdateCancelResponse) Status() string {
if r.HTTPResponse != nil {
return r.HTTPResponse.Status
}
return http.StatusText(0)
}
// StatusCode returns HTTPResponse.StatusCode
func (r EventUpdateCancelResponse) StatusCode() int {
if r.HTTPResponse != nil {
return r.HTTPResponse.StatusCode
}
return 0
}
type EventKeyListResponse struct {
Body []byte
HTTPResponse *http.Response
@@ -9156,6 +9291,23 @@ func (c *ClientWithResponses) EventCreateWithResponse(ctx context.Context, tenan
return ParseEventCreateResponse(rsp)
}
// EventUpdateCancelWithBodyWithResponse request with arbitrary body returning *EventUpdateCancelResponse
func (c *ClientWithResponses) EventUpdateCancelWithBodyWithResponse(ctx context.Context, tenant openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*EventUpdateCancelResponse, error) {
rsp, err := c.EventUpdateCancelWithBody(ctx, tenant, contentType, body, reqEditors...)
if err != nil {
return nil, err
}
return ParseEventUpdateCancelResponse(rsp)
}
func (c *ClientWithResponses) EventUpdateCancelWithResponse(ctx context.Context, tenant openapi_types.UUID, body EventUpdateCancelJSONRequestBody, reqEditors ...RequestEditorFn) (*EventUpdateCancelResponse, error) {
rsp, err := c.EventUpdateCancel(ctx, tenant, body, reqEditors...)
if err != nil {
return nil, err
}
return ParseEventUpdateCancelResponse(rsp)
}
// EventKeyListWithResponse request returning *EventKeyListResponse
func (c *ClientWithResponses) EventKeyListWithResponse(ctx context.Context, tenant openapi_types.UUID, reqEditors ...RequestEditorFn) (*EventKeyListResponse, error) {
rsp, err := c.EventKeyList(ctx, tenant, reqEditors...)
@@ -10691,6 +10843,55 @@ func ParseEventCreateResponse(rsp *http.Response) (*EventCreateResponse, error)
return response, nil
}
// ParseEventUpdateCancelResponse parses an HTTP response from a EventUpdateCancelWithResponse call
func ParseEventUpdateCancelResponse(rsp *http.Response) (*EventUpdateCancelResponse, error) {
bodyBytes, err := io.ReadAll(rsp.Body)
defer func() { _ = rsp.Body.Close() }()
if err != nil {
return nil, err
}
response := &EventUpdateCancelResponse{
Body: bodyBytes,
HTTPResponse: rsp,
}
switch {
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
var dest struct {
WorkflowRunIds *[]openapi_types.UUID `json:"workflowRunIds,omitempty"`
}
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
return nil, err
}
response.JSON200 = &dest
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
var dest APIErrors
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
return nil, err
}
response.JSON400 = &dest
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
var dest APIErrors
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
return nil, err
}
response.JSON403 = &dest
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 429:
var dest APIErrors
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
return nil, err
}
response.JSON429 = &dest
}
return response, nil
}
// ParseEventKeyListResponse parses an HTTP response from a EventKeyListWithResponse call
func ParseEventKeyListResponse(rsp *http.Response) (*EventKeyListResponse, error) {
bodyBytes, err := io.ReadAll(rsp.Body)
+3
View File
@@ -55,6 +55,9 @@ type ListEventOpts struct {
// (optional) the event metadata
AdditionalMetadata []byte
// (optional) event ids to filter by
Ids []string
}
type ListEventResult struct {
+41 -17
View File
@@ -24,6 +24,10 @@ WITH events AS (
WHERE
events."tenantId" = $1 AND
events."deletedAt" IS NULL AND
(
sqlc.narg('event_ids')::uuid[] IS NULL OR
events."id" = ANY(sqlc.narg('event_ids')::uuid[])
) AND
(
sqlc.narg('keys')::text[] IS NULL OR
events."key" = ANY(sqlc.narg('keys')::text[])
@@ -95,11 +99,15 @@ WITH filtered_events AS (
WHERE
events."tenantId" = $1 AND
events."deletedAt" IS NULL AND
(
sqlc.narg('event_ids')::uuid[] IS NULL OR
events."id" = ANY(sqlc.narg('event_ids')::uuid[])
) AND
(
sqlc.narg('keys')::text[] IS NULL OR
events."key" = ANY(sqlc.narg('keys')::text[])
) AND
(
(
sqlc.narg('additionalMetadata')::jsonb IS NULL OR
events."additionalMetadata" @> sqlc.narg('additionalMetadata')::jsonb
) AND
@@ -114,35 +122,51 @@ WITH filtered_events AS (
) AND
(
sqlc.narg('statuses')::text[] IS NULL OR
"status" = ANY(cast(sqlc.narg('statuses')::text[] as "WorkflowRunStatus"[]))
runs."status" = ANY(cast(sqlc.narg('statuses')::text[] as "WorkflowRunStatus"[]))
)
GROUP BY events."id"
ORDER BY
case when @orderBy = 'createdAt ASC' THEN events."createdAt" END ASC ,
case when @orderBy = 'createdAt DESC' then events."createdAt" END DESC
case when @orderBy = 'createdAt ASC' THEN MAX(events."createdAt") END ASC,
case when @orderBy = 'createdAt DESC' then MAX(events."createdAt") END DESC
OFFSET
COALESCE(sqlc.narg('offset'), 0)
LIMIT
COALESCE(sqlc.narg('limit'), 50)
),
event_run_counts AS (
SELECT
events."id" as event_id,
COUNT(CASE WHEN runs."status" = 'PENDING' THEN 1 END) AS pendingRuns,
COUNT(CASE WHEN runs."status" = 'QUEUED' THEN 1 END) AS queuedRuns,
COUNT(CASE WHEN runs."status" = 'RUNNING' THEN 1 END) AS runningRuns,
COUNT(CASE WHEN runs."status" = 'SUCCEEDED' THEN 1 END) AS succeededRuns,
COUNT(CASE WHEN runs."status" = 'FAILED' THEN 1 END) AS failedRuns
FROM
filtered_events
JOIN
"Event" as events ON events."id" = filtered_events."id"
LEFT JOIN
"WorkflowRunTriggeredBy" as runTriggers ON events."id" = runTriggers."eventId"
LEFT JOIN
"WorkflowRun" as runs ON runTriggers."parentId" = runs."id"
GROUP BY
events."id"
)
SELECT
sqlc.embed(events),
sum(case when runs."status" = 'PENDING' then 1 else 0 end) AS pendingRuns,
sum(case when runs."status" = 'QUEUED' then 1 else 0 end) AS queuedRuns,
sum(case when runs."status" = 'RUNNING' then 1 else 0 end) AS runningRuns,
sum(case when runs."status" = 'SUCCEEDED' then 1 else 0 end) AS succeededRuns,
sum(case when runs."status" = 'FAILED' then 1 else 0 end) AS failedRuns
COALESCE(erc.pendingRuns, 0) AS pendingRuns,
COALESCE(erc.queuedRuns, 0) AS queuedRuns,
COALESCE(erc.runningRuns, 0) AS runningRuns,
COALESCE(erc.succeededRuns, 0) AS succeededRuns,
COALESCE(erc.failedRuns, 0) AS failedRuns
FROM
filtered_events
filtered_events fe
JOIN
"Event" as events ON events."id" = filtered_events."id"
"Event" as events ON events."id" = fe."id"
LEFT JOIN
"WorkflowRunTriggeredBy" as runTriggers ON events."id" = runTriggers."eventId"
LEFT JOIN
"WorkflowRun" as runs ON runTriggers."parentId" = runs."id"
GROUP BY
events."id", events."createdAt"
event_run_counts erc ON events."id" = erc.event_id
ORDER BY
case when @orderBy = 'createdAt ASC' THEN events."createdAt" END ASC ,
case when @orderBy = 'createdAt ASC' THEN events."createdAt" END ASC,
case when @orderBy = 'createdAt DESC' then events."createdAt" END DESC;
-- name: GetEventsForRange :many
+87 -59
View File
@@ -76,29 +76,33 @@ WITH events AS (
events."tenantId" = $1 AND
events."deletedAt" IS NULL AND
(
$2::text[] IS NULL OR
events."key" = ANY($2::text[])
$2::uuid[] IS NULL OR
events."id" = ANY($2::uuid[])
) AND
(
$3::jsonb IS NULL OR
events."additionalMetadata" @> $3::jsonb
$3::text[] IS NULL OR
events."key" = ANY($3::text[])
) AND
(
($4::text[])::uuid[] IS NULL OR
(workflow."id" = ANY($4::text[]::uuid[]))
$4::jsonb IS NULL OR
events."additionalMetadata" @> $4::jsonb
) AND
(
$5::text IS NULL OR
workflow.name like concat('%', $5::text, '%') OR
jsonb_path_exists(events."data", cast(concat('$.** ? (@.type() == "string" && @ like_regex "', $5::text, '")') as jsonpath))
($5::text[])::uuid[] IS NULL OR
(workflow."id" = ANY($5::text[]::uuid[]))
) AND
(
$6::text[] IS NULL OR
"status" = ANY(cast($6::text[] as "WorkflowRunStatus"[]))
$6::text IS NULL OR
workflow.name like concat('%', $6::text, '%') OR
jsonb_path_exists(events."data", cast(concat('$.** ? (@.type() == "string" && @ like_regex "', $6::text, '")') as jsonpath))
) AND
(
$7::text[] IS NULL OR
"status" = ANY(cast($7::text[] as "WorkflowRunStatus"[]))
)
ORDER BY
case when $7 = 'createdAt ASC' THEN events."createdAt" END ASC ,
case when $7 = 'createdAt DESC' then events."createdAt" END DESC
case when $8 = 'createdAt ASC' THEN events."createdAt" END ASC ,
case when $8 = 'createdAt DESC' then events."createdAt" END DESC
LIMIT 10000
)
SELECT
@@ -108,18 +112,20 @@ FROM
`
type CountEventsParams struct {
TenantId pgtype.UUID `json:"tenantId"`
Keys []string `json:"keys"`
AdditionalMetadata []byte `json:"additionalMetadata"`
Workflows []string `json:"workflows"`
Search pgtype.Text `json:"search"`
Statuses []string `json:"statuses"`
Orderby interface{} `json:"orderby"`
TenantId pgtype.UUID `json:"tenantId"`
EventIds []pgtype.UUID `json:"event_ids"`
Keys []string `json:"keys"`
AdditionalMetadata []byte `json:"additionalMetadata"`
Workflows []string `json:"workflows"`
Search pgtype.Text `json:"search"`
Statuses []string `json:"statuses"`
Orderby interface{} `json:"orderby"`
}
func (q *Queries) CountEvents(ctx context.Context, db DBTX, arg CountEventsParams) (int64, error) {
row := db.QueryRow(ctx, countEvents,
arg.TenantId,
arg.EventIds,
arg.Keys,
arg.AdditionalMetadata,
arg.Workflows,
@@ -280,66 +286,87 @@ WITH filtered_events AS (
events."tenantId" = $1 AND
events."deletedAt" IS NULL AND
(
$3::text[] IS NULL OR
events."key" = ANY($3::text[])
) AND
(
$4::jsonb IS NULL OR
events."additionalMetadata" @> $4::jsonb
$3::uuid[] IS NULL OR
events."id" = ANY($3::uuid[])
) AND
(
($5::text[])::uuid[] IS NULL OR
(workflow."id" = ANY($5::text[]::uuid[]))
$4::text[] IS NULL OR
events."key" = ANY($4::text[])
) AND
(
$6::text IS NULL OR
workflow.name like concat('%', $6::text, '%') OR
jsonb_path_exists(events."data", cast(concat('$.** ? (@.type() == "string" && @ like_regex "', $6::text, '")') as jsonpath))
$5::jsonb IS NULL OR
events."additionalMetadata" @> $5::jsonb
) AND
(
$7::text[] IS NULL OR
"status" = ANY(cast($7::text[] as "WorkflowRunStatus"[]))
($6::text[])::uuid[] IS NULL OR
(workflow."id" = ANY($6::text[]::uuid[]))
) AND
(
$7::text IS NULL OR
workflow.name like concat('%', $7::text, '%') OR
jsonb_path_exists(events."data", cast(concat('$.** ? (@.type() == "string" && @ like_regex "', $7::text, '")') as jsonpath))
) AND
(
$8::text[] IS NULL OR
runs."status" = ANY(cast($8::text[] as "WorkflowRunStatus"[]))
)
GROUP BY events."id"
ORDER BY
case when $2 = 'createdAt ASC' THEN events."createdAt" END ASC ,
case when $2 = 'createdAt DESC' then events."createdAt" END DESC
case when $2 = 'createdAt ASC' THEN MAX(events."createdAt") END ASC,
case when $2 = 'createdAt DESC' then MAX(events."createdAt") END DESC
OFFSET
COALESCE($8, 0)
COALESCE($9, 0)
LIMIT
COALESCE($9, 50)
COALESCE($10, 50)
),
event_run_counts AS (
SELECT
events."id" as event_id,
COUNT(CASE WHEN runs."status" = 'PENDING' THEN 1 END) AS pendingRuns,
COUNT(CASE WHEN runs."status" = 'QUEUED' THEN 1 END) AS queuedRuns,
COUNT(CASE WHEN runs."status" = 'RUNNING' THEN 1 END) AS runningRuns,
COUNT(CASE WHEN runs."status" = 'SUCCEEDED' THEN 1 END) AS succeededRuns,
COUNT(CASE WHEN runs."status" = 'FAILED' THEN 1 END) AS failedRuns
FROM
filtered_events
JOIN
"Event" as events ON events."id" = filtered_events."id"
LEFT JOIN
"WorkflowRunTriggeredBy" as runTriggers ON events."id" = runTriggers."eventId"
LEFT JOIN
"WorkflowRun" as runs ON runTriggers."parentId" = runs."id"
GROUP BY
events."id"
)
SELECT
events.id, events."createdAt", events."updatedAt", events."deletedAt", events.key, events."tenantId", events."replayedFromId", events.data, events."additionalMetadata",
sum(case when runs."status" = 'PENDING' then 1 else 0 end) AS pendingRuns,
sum(case when runs."status" = 'QUEUED' then 1 else 0 end) AS queuedRuns,
sum(case when runs."status" = 'RUNNING' then 1 else 0 end) AS runningRuns,
sum(case when runs."status" = 'SUCCEEDED' then 1 else 0 end) AS succeededRuns,
sum(case when runs."status" = 'FAILED' then 1 else 0 end) AS failedRuns
COALESCE(erc.pendingRuns, 0) AS pendingRuns,
COALESCE(erc.queuedRuns, 0) AS queuedRuns,
COALESCE(erc.runningRuns, 0) AS runningRuns,
COALESCE(erc.succeededRuns, 0) AS succeededRuns,
COALESCE(erc.failedRuns, 0) AS failedRuns
FROM
filtered_events
filtered_events fe
JOIN
"Event" as events ON events."id" = filtered_events."id"
"Event" as events ON events."id" = fe."id"
LEFT JOIN
"WorkflowRunTriggeredBy" as runTriggers ON events."id" = runTriggers."eventId"
LEFT JOIN
"WorkflowRun" as runs ON runTriggers."parentId" = runs."id"
GROUP BY
events."id", events."createdAt"
event_run_counts erc ON events."id" = erc.event_id
ORDER BY
case when $2 = 'createdAt ASC' THEN events."createdAt" END ASC ,
case when $2 = 'createdAt ASC' THEN events."createdAt" END ASC,
case when $2 = 'createdAt DESC' then events."createdAt" END DESC
`
type ListEventsParams struct {
TenantId pgtype.UUID `json:"tenantId"`
Orderby interface{} `json:"orderby"`
Keys []string `json:"keys"`
AdditionalMetadata []byte `json:"additionalMetadata"`
Workflows []string `json:"workflows"`
Search pgtype.Text `json:"search"`
Statuses []string `json:"statuses"`
Offset interface{} `json:"offset"`
Limit interface{} `json:"limit"`
TenantId pgtype.UUID `json:"tenantId"`
Orderby interface{} `json:"orderby"`
EventIds []pgtype.UUID `json:"event_ids"`
Keys []string `json:"keys"`
AdditionalMetadata []byte `json:"additionalMetadata"`
Workflows []string `json:"workflows"`
Search pgtype.Text `json:"search"`
Statuses []string `json:"statuses"`
Offset interface{} `json:"offset"`
Limit interface{} `json:"limit"`
}
type ListEventsRow struct {
@@ -355,6 +382,7 @@ func (q *Queries) ListEvents(ctx context.Context, db DBTX, arg ListEventsParams)
rows, err := db.Query(ctx, listEvents,
arg.TenantId,
arg.Orderby,
arg.EventIds,
arg.Keys,
arg.AdditionalMetadata,
arg.Workflows,
@@ -176,6 +176,10 @@ WHERE
runs."deletedAt" IS NULL AND
workflowVersion."deletedAt" IS NULL AND
workflow."deletedAt" IS NULL AND
(
sqlc.narg('eventId')::uuid IS NULL OR
events."id" = sqlc.narg('eventId')::uuid
) AND
(
sqlc.narg('ids')::uuid[] IS NULL OR
runs."id" = ANY(sqlc.narg('ids')::uuid[])
@@ -1409,6 +1409,10 @@ WHERE
runs."deletedAt" IS NULL AND
workflowVersion."deletedAt" IS NULL AND
workflow."deletedAt" IS NULL AND
(
$2::uuid IS NULL OR
events."id" = $2::uuid
) AND
(
$6::uuid[] IS NULL OR
runs."id" = ANY($6::uuid[])
+10
View File
@@ -62,6 +62,16 @@ func (r *eventAPIRepository) ListEvents(ctx context.Context, tenantId string, op
TenantId: *pgTenantId,
}
if opts.Ids != nil {
queryParams.EventIds = make([]pgtype.UUID, len(opts.Ids))
countParams.EventIds = make([]pgtype.UUID, len(opts.Ids))
for i := range opts.Ids {
queryParams.EventIds[i] = sqlchelpers.UUIDFromStr(opts.Ids[i])
countParams.EventIds[i] = sqlchelpers.UUIDFromStr(opts.Ids[i])
}
}
if opts.Search != nil {
queryParams.Search = sqlchelpers.TextFromStr(*opts.Search)
countParams.Search = sqlchelpers.TextFromStr(*opts.Search)