diff --git a/pkg/coreapi/generated/generated.go b/pkg/coreapi/generated/generated.go index 0455440238..4943830da4 100644 --- a/pkg/coreapi/generated/generated.go +++ b/pkg/coreapi/generated/generated.go @@ -110,6 +110,7 @@ type ComplexityRoot struct { FunctionRun struct { BatchCreatedAt func(childComplexity int) int BatchID func(childComplexity int) int + Cron func(childComplexity int) int Event func(childComplexity int) int EventID func(childComplexity int) int Events func(childComplexity int) int @@ -606,6 +607,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.FunctionRun.BatchID(childComplexity), true + case "FunctionRun.cron": + if e.complexity.FunctionRun.Cron == nil { + break + } + + return e.complexity.FunctionRun.Cron(childComplexity), true + case "FunctionRun.event": if e.complexity.FunctionRun.Event == nil { break @@ -1684,6 +1692,7 @@ type FunctionRun { history: [RunHistoryItem!]! historyItemOutput(id: ULID!): String eventID: ID! + cron: String } enum HistoryType { @@ -3086,6 +3095,8 @@ func (ec *executionContext) fieldContext_Event_functionRuns(ctx context.Context, return ec.fieldContext_FunctionRun_historyItemOutput(ctx, field) case "eventID": return ec.fieldContext_FunctionRun_eventID(ctx, field) + case "cron": + return ec.fieldContext_FunctionRun_cron(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type FunctionRun", field.Name) }, @@ -3633,6 +3644,8 @@ func (ec *executionContext) fieldContext_FunctionEvent_functionRun(ctx context.C return ec.fieldContext_FunctionRun_historyItemOutput(ctx, field) case "eventID": return ec.fieldContext_FunctionRun_eventID(ctx, field) + case "cron": + return ec.fieldContext_FunctionRun_cron(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type FunctionRun", field.Name) }, @@ -4604,6 +4617,47 @@ func (ec *executionContext) fieldContext_FunctionRun_eventID(ctx context.Context return fc, nil } +func (ec *executionContext) _FunctionRun_cron(ctx context.Context, field graphql.CollectedField, obj *models.FunctionRun) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_FunctionRun_cron(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Cron, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2áš–string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_FunctionRun_cron(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "FunctionRun", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _FunctionTrigger_type(ctx context.Context, field graphql.CollectedField, obj *models.FunctionTrigger) (ret graphql.Marshaler) { fc, err := ec.fieldContext_FunctionTrigger_type(ctx, field) if err != nil { @@ -5391,6 +5445,8 @@ func (ec *executionContext) fieldContext_Mutation_cancelRun(ctx context.Context, return ec.fieldContext_FunctionRun_historyItemOutput(ctx, field) case "eventID": return ec.fieldContext_FunctionRun_eventID(ctx, field) + case "cron": + return ec.fieldContext_FunctionRun_cron(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type FunctionRun", field.Name) }, @@ -5890,6 +5946,8 @@ func (ec *executionContext) fieldContext_Query_functionRun(ctx context.Context, return ec.fieldContext_FunctionRun_historyItemOutput(ctx, field) case "eventID": return ec.fieldContext_FunctionRun_eventID(ctx, field) + case "cron": + return ec.fieldContext_FunctionRun_cron(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type FunctionRun", field.Name) }, @@ -7865,6 +7923,8 @@ func (ec *executionContext) fieldContext_StepEvent_functionRun(ctx context.Conte return ec.fieldContext_FunctionRun_historyItemOutput(ctx, field) case "eventID": return ec.fieldContext_FunctionRun_eventID(ctx, field) + case "cron": + return ec.fieldContext_FunctionRun_cron(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type FunctionRun", field.Name) }, @@ -8498,6 +8558,8 @@ func (ec *executionContext) fieldContext_StreamItem_runs(ctx context.Context, fi return ec.fieldContext_FunctionRun_historyItemOutput(ctx, field) case "eventID": return ec.fieldContext_FunctionRun_eventID(ctx, field) + case "cron": + return ec.fieldContext_FunctionRun_cron(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type FunctionRun", field.Name) }, @@ -11366,6 +11428,10 @@ func (ec *executionContext) _FunctionRun(ctx context.Context, sel ast.SelectionS if out.Values[i] == graphql.Null { atomic.AddUint32(&invalids, 1) } + case "cron": + + out.Values[i] = ec._FunctionRun_cron(ctx, field, obj) + default: panic("unknown field " + strconv.Quote(field.Name)) } diff --git a/pkg/coreapi/gql.schema.graphql b/pkg/coreapi/gql.schema.graphql index ba07322743..580bc74027 100644 --- a/pkg/coreapi/gql.schema.graphql +++ b/pkg/coreapi/gql.schema.graphql @@ -200,6 +200,7 @@ type FunctionRun { history: [RunHistoryItem!]! historyItemOutput(id: ULID!): String eventID: ID! + cron: String } enum HistoryType { diff --git a/pkg/coreapi/graph/models/converters.go b/pkg/coreapi/graph/models/converters.go index 8c5b6b41e1..38ff8bd11c 100644 --- a/pkg/coreapi/graph/models/converters.go +++ b/pkg/coreapi/graph/models/converters.go @@ -72,6 +72,7 @@ func MakeFunctionRun(f *cqrs.FunctionRun) *FunctionRun { EventID: f.EventID.String(), BatchID: f.BatchID, Status: &status, + Cron: f.Cron, } if len(f.Output) > 0 { str := string(f.Output) diff --git a/pkg/coreapi/graph/models/models_gen.go b/pkg/coreapi/graph/models/models_gen.go index 27aa1178a5..1c8bc703a7 100644 --- a/pkg/coreapi/graph/models/models_gen.go +++ b/pkg/coreapi/graph/models/models_gen.go @@ -92,6 +92,7 @@ type FunctionRun struct { History []*history_reader.RunHistory `json:"history"` HistoryItemOutput *string `json:"historyItemOutput,omitempty"` EventID string `json:"eventID"` + Cron *string `json:"cron,omitempty"` } type FunctionRunQuery struct { diff --git a/pkg/coreapi/graph/resolvers/function_run.resolver.go b/pkg/coreapi/graph/resolvers/function_run.resolver.go index ee89d2d8f7..cabd119c9f 100644 --- a/pkg/coreapi/graph/resolvers/function_run.resolver.go +++ b/pkg/coreapi/graph/resolvers/function_run.resolver.go @@ -10,7 +10,6 @@ import ( "github.com/google/uuid" "github.com/inngest/inngest/pkg/coreapi/graph/models" - "github.com/inngest/inngest/pkg/cqrs" "github.com/inngest/inngest/pkg/enums" "github.com/inngest/inngest/pkg/event" "github.com/inngest/inngest/pkg/execution" @@ -280,18 +279,17 @@ func (r *mutationResolver) Rerun( } evt, err := r.Data.GetEventByInternalID(ctx, run.EventID) - if run.Cron != nil && err == sql.ErrNoRows { - // Create a dummy event since we don't store cron events. We can delete - // this dummy when we start storing cron events - evt = &cqrs.Event{} - } else if err != nil { + if err != nil { return zero, fmt.Errorf("failed to get run event: %w", err) } identifier, err := r.Executor.Schedule(ctx, execution.ScheduleRequest{ Function: *fn, Events: []event.TrackedEvent{ - event.NewOSSTrackedEvent(evt.Event()), + // We need NewOSSTrackedEventWithID to ensure that the tracked event + // has the same ID as the original event. Calling NewOSSTrackedEvent + // will result in the creation of a new ID + event.NewOSSTrackedEventWithID(evt.Event(), evt.InternalID()), }, OriginalRunID: &run.RunID, }) diff --git a/pkg/coreapi/graph/resolvers/stream.go b/pkg/coreapi/graph/resolvers/stream.go index 448cb4b58d..f1ab6b5bb5 100644 --- a/pkg/coreapi/graph/resolvers/stream.go +++ b/pkg/coreapi/graph/resolvers/stream.go @@ -73,44 +73,20 @@ func (r *queryResolver) Stream(ctx context.Context, q models.StreamQuery) ([]*mo CreatedAt: time.UnixMilli(i.EventTS), Runs: []*models.FunctionRun{}, } - if len(fnsByID[i.ID]) > 0 { - items[n].Runs = fnsByID[i.ID] - } - } - - // Query all function runs received, and filter by crons. - fns, err = r.Data.GetFunctionRunsTimebound(ctx, tb, q.Limit) - if err != nil { - return nil, err - } - for _, i := range fns { - if i.Cron == nil && i.OriginalRunID == nil { - // These are children of events. - continue - } - var trigger string - if i.Cron != nil { - trigger = *i.Cron - } else if i.OriginalRunID != nil { - trigger = "Cron rerun" + runs := fnsByID[i.ID] + if len(runs) > 0 { + // If any of the runs is a cron, then the stream item is a cron + for _, run := range runs { + if run.Cron != nil { + items[n].Trigger = *run.Cron + items[n].Type = models.StreamTypeCron + break + } + } + + items[n].Runs = runs } - - runs := []*models.FunctionRun{models.MakeFunctionRun(i)} - _, err := r.Data.GetFunctionByInternalUUID(ctx, uuid.UUID{}, uuid.MustParse(runs[0].FunctionID)) - if err == sql.ErrNoRows { - // Skip run since its function doesn't exist. This can happen when - // deleting a function or changing its ID. - runs = []*models.FunctionRun{} - } - - items = append(items, &models.StreamItem{ - ID: i.RunID.String(), - Trigger: trigger, - Type: models.StreamTypeCron, - CreatedAt: i.RunStartedAt, - Runs: runs, - }) } sort.Slice(items, func(i, j int) bool { diff --git a/pkg/devserver/devserver.go b/pkg/devserver/devserver.go index 2b37954c53..67dc73fe63 100644 --- a/pkg/devserver/devserver.go +++ b/pkg/devserver/devserver.go @@ -250,6 +250,7 @@ func start(ctx context.Context, opts StartOpts) error { runner.WithTracker(t), runner.WithRateLimiter(rl), runner.WithBatchManager(batcher), + runner.WithPublisher(pb), ) // The devserver embeds the event API. diff --git a/pkg/event/event.go b/pkg/event/event.go index 991de6b26c..5001176768 100644 --- a/pkg/event/event.go +++ b/pkg/event/event.go @@ -168,6 +168,13 @@ func NewOSSTrackedEvent(e Event) TrackedEvent { } } +func NewOSSTrackedEventWithID(e Event, id ulid.ULID) TrackedEvent { + return ossTrackedEvent{ + Id: id, + Event: e, + } +} + func NewOSSTrackedEventFromString(data string) (*ossTrackedEvent, error) { evt := &ossTrackedEvent{} if err := json.Unmarshal([]byte(data), evt); err != nil { diff --git a/pkg/execution/runner/runner.go b/pkg/execution/runner/runner.go index 4ee179a619..aa399b12de 100644 --- a/pkg/execution/runner/runner.go +++ b/pkg/execution/runner/runner.go @@ -3,6 +3,7 @@ package runner import ( "bytes" "context" + "encoding/json" "fmt" "sync" "time" @@ -106,6 +107,12 @@ func WithTracker(t *Tracker) func(s *svc) { } } +func WithPublisher(p pubsub.Publisher) func(s *svc) { + return func(s *svc) { + s.publisher = p + } +} + func NewService(c config.Config, opts ...Opt) Runner { svc := &svc{config: c} for _, o := range opts { @@ -119,7 +126,8 @@ type svc struct { cqrs cqrs.Manager // pubsub allows us to subscribe to new events, and re-publish events // if there are errors. - pubsub pubsub.PublishSubscriber + pubsub pubsub.PublishSubscriber + publisher pubsub.Publisher // executor handles execution of functions. executor execution.Executor // data provides the required loading capabilities to trigger functions @@ -252,13 +260,33 @@ func (s *svc) InitializeCrons(ctx context.Context) error { )) defer span.End() - err := s.initialize(ctx, fn, event.NewOSSTrackedEvent(event.Event{ + trackedEvent := event.NewOSSTrackedEvent(event.Event{ Data: map[string]any{ "cron": cron, }, ID: time.Now().UTC().Format(time.RFC3339), Name: event.FnCronName, - })) + }) + + byt, err := json.Marshal(trackedEvent) + if err == nil { + err := s.publisher.Publish( + ctx, + s.config.EventStream.Service.TopicName(), + pubsub.Message{ + Name: event.EventReceivedName, + Data: string(byt), + Timestamp: time.Now(), + }, + ) + if err != nil { + logger.From(ctx).Error().Err(err).Msg("error publishing cron event") + } + } else { + logger.From(ctx).Error().Err(err).Msg("error marshaling cron event") + } + + err = s.initialize(ctx, fn, trackedEvent) if err != nil { logger.From(ctx).Error().Err(err).Msg("error initializing scheduled function") } diff --git a/pkg/history_drivers/memory_writer/writer.go b/pkg/history_drivers/memory_writer/writer.go index 7f64829d75..90ffb937d6 100644 --- a/pkg/history_drivers/memory_writer/writer.go +++ b/pkg/history_drivers/memory_writer/writer.go @@ -88,6 +88,7 @@ func (w *writer) writeWorkflowStart( run := w.store.Data[item.RunID] run.Run.AccountID = item.AccountID run.Run.BatchID = item.BatchID + run.Run.Cron = item.Cron run.Run.EventID = item.EventID run.Run.ID = item.RunID run.Run.OriginalRunID = item.OriginalRunID diff --git a/ui/apps/dashboard/src/app/(organization-active)/(dashboard)/env/[environmentSlug]/functions/[slug]/logs/(run)/[runId]/StreamDetails.tsx b/ui/apps/dashboard/src/app/(organization-active)/(dashboard)/env/[environmentSlug]/functions/[slug]/logs/(run)/[runId]/StreamDetails.tsx index dc72b1f9d2..f661de8482 100644 --- a/ui/apps/dashboard/src/app/(organization-active)/(dashboard)/env/[environmentSlug]/functions/[slug]/logs/(run)/[runId]/StreamDetails.tsx +++ b/ui/apps/dashboard/src/app/(organization-active)/(dashboard)/env/[environmentSlug]/functions/[slug]/logs/(run)/[runId]/StreamDetails.tsx @@ -2,6 +2,7 @@ import { useMemo } from 'react'; import type { Route } from 'next'; +import { useRouter } from 'next/navigation'; import { EventDetails } from '@inngest/components/EventDetails'; import { Link } from '@inngest/components/Link'; import { RunDetails } from '@inngest/components/RunDetails'; @@ -19,7 +20,6 @@ import { useClient, useMutation } from 'urql'; import { graphql } from '@/gql'; import { devServerURL, useDevServer } from '@/utils/useDevServer'; -import RerunButton from './RerunButton'; import { getHistoryItemOutput } from './getHistoryItemOutput'; const CancelRunDocument = graphql(` @@ -52,7 +52,14 @@ export function StreamDetails({ }: Props) { const client = useClient(); const { isRunning, send } = useDevServer(); - const [, cancelRun] = useMutation(CancelRunDocument); + const cancelRun = useCancelRun({ envID: environment.id, runID: run.id }); + const rerun = useRerun({ + envID: environment.id, + envSlug: environment.slug, + fnID: func.id, + fnSlug: func.slug, + runID: run.id, + }); const getOutput = useMemo(() => { return (historyItemID: string) => { @@ -68,11 +75,6 @@ export function StreamDetails({ const history = useParsedHistory(rawHistory); - let rerunButton: React.ReactNode | undefined; - if (run.canRerun) { - rerunButton = ; - } - const navigateToRun: NavigateToRunFn = (opts) => { return ( )} { - const res = await cancelRun({ envID: environment.id, runID: run.id }); - if (res.error) { - // Throw error so that the modal can catch and display it - throw res.error; - } - }} + cancelRun={cancelRun} func={func} functionVersion={functionVersion} getHistoryItemOutput={getOutput} history={history} - rerunButton={rerunButton} + rerun={rerun} run={run} navigateToRun={navigateToRun} /> ); } + +function useCancelRun({ envID, runID }: { envID: string; runID: string }) { + const [, mutate] = useMutation(CancelRunDocument); + + return async () => { + const res = await mutate({ envID, runID }); + if (res.error) { + // Throw error so that the modal can catch and display it + throw res.error; + } + }; +} + +const RerunFunctionRunDocument = graphql(/* GraphQL */ ` + mutation RerunFunctionRun($environmentID: ID!, $functionID: ID!, $functionRunID: ULID!) { + retryWorkflowRun( + input: { workspaceID: $environmentID, workflowID: $functionID } + workflowRunID: $functionRunID + ) { + id + } + } +`); + +function useRerun({ + envID, + envSlug, + fnID, + fnSlug, + runID, +}: { + envID: string; + envSlug: string; + fnID: string; + fnSlug: string; + runID: string; +}) { + const [, rerunFunctionRunMutation] = useMutation(RerunFunctionRunDocument); + const router = useRouter(); + + return async () => { + const response = await rerunFunctionRunMutation({ + environmentID: envID, + functionID: fnID, + functionRunID: runID, + }); + if (response.error) { + throw response.error; + } + const newRunID = response.data?.retryWorkflowRun?.id; + if (!newRunID) { + throw new Error('missing new run ID'); + } + + router.refresh(); + router.push( + `/env/${envSlug}/functions/${encodeURIComponent(fnSlug)}/logs/${newRunID}` as Route + ); + }; +} diff --git a/ui/apps/dev-server-ui/src/app/(dashboard)/stream/FunctionRunList.tsx b/ui/apps/dev-server-ui/src/app/(dashboard)/stream/FunctionRunList.tsx index 531008830c..4d105e1d49 100644 --- a/ui/apps/dev-server-ui/src/app/(dashboard)/stream/FunctionRunList.tsx +++ b/ui/apps/dev-server-ui/src/app/(dashboard)/stream/FunctionRunList.tsx @@ -1,6 +1,6 @@ import { Badge } from '@inngest/components/Badge'; -import { FunctionRunStatusIcon } from '@inngest/components/FunctionRunStatusIcon'; import { BatchSize } from '@inngest/components/BatchSize'; +import { FunctionRunStatusIcon } from '@inngest/components/FunctionRunStatusIcon'; import { useGetFunctionRunStatusQuery, type FunctionRun } from '@/store/generated'; @@ -19,7 +19,14 @@ export default function FunctionRunList({ inBatch, functionRuns }: FunctionRunLi {functionRuns && functionRuns ?.slice() - .sort((a, b) => (a.function?.name || '').localeCompare(b.function?.name || '')) + .sort((a, b) => { + // Append with run ID to ensure unique keys. Rerunning + // intentionally results in duplicate function names + const aVal = `${a.function?.name || ''}${a.id}`; + const bVal = `${b.function?.name || ''}${b.id}`; + + return aVal.localeCompare(bVal); + }) .map((functionRun) => { let batchSize; if (functionRun.batchID) { diff --git a/ui/apps/dev-server-ui/src/app/(dashboard)/stream/Stream.tsx b/ui/apps/dev-server-ui/src/app/(dashboard)/stream/Stream.tsx index e60cb230fc..ed9eefb426 100644 --- a/ui/apps/dev-server-ui/src/app/(dashboard)/stream/Stream.tsx +++ b/ui/apps/dev-server-ui/src/app/(dashboard)/stream/Stream.tsx @@ -195,20 +195,16 @@ export default function Stream() { function handleOpenSlideOver({ triggerID, - isCron, e, firstRunID, }: { triggerID: string; - isCron: boolean; e: React.MouseEvent; firstRunID?: string; }) { if (e.target instanceof HTMLElement) { const runID = e.target.dataset.key || firstRunID; - const params = new URLSearchParams({ - [isCron ? 'cron' : 'event']: triggerID, - }); + const params = new URLSearchParams({ event: triggerID }); if (runID) { params.append('run', runID); } @@ -223,14 +219,12 @@ export default function Stream() { cursor: 'pointer', }, onClick: (e: React.MouseEvent) => { - const isCron = row.original.type === 'CRON'; const firstRunID = row.original.runs && row.original.runs?.length > 0 ? row.original.runs[0]?.id : undefined; handleOpenSlideOver({ triggerID: row.original.id, e, firstRunID: firstRunID, - isCron: isCron, }); }, }); diff --git a/ui/apps/dev-server-ui/src/app/(dashboard)/stream/StreamDetails.tsx b/ui/apps/dev-server-ui/src/app/(dashboard)/stream/StreamDetails.tsx index f823ab3ff8..193ba28774 100644 --- a/ui/apps/dev-server-ui/src/app/(dashboard)/stream/StreamDetails.tsx +++ b/ui/apps/dev-server-ui/src/app/(dashboard)/stream/StreamDetails.tsx @@ -11,7 +11,7 @@ import { ulid } from 'ulid'; import SendEventButton from '@/components/Event/SendEventButton'; import { useSendEventMutation } from '@/store/devApi'; -import { useCancelRunMutation } from '@/store/generated'; +import { useCancelRunMutation, useRerunMutation } from '@/store/generated'; import { useEvent } from './useEvent'; import { useGetHistoryItemOutput } from './useGetHistoryItemOutput'; import { useRun } from './useRun'; @@ -21,6 +21,7 @@ export default function StreamDetails() { const eventID = params.get('event'); const runID = params.get('run'); const [cancelRun] = useCancelRunMutation(); + const [rerun] = useRerunMutation(); const eventResult = useEvent(eventID); useEffect(() => { @@ -156,6 +157,13 @@ export default function StreamDetails() { func={runResult.data.func} getHistoryItemOutput={getHistoryItemOutput} history={runResult.data.history} + rerun={async () => { + const res = await rerun({ runID: runResult.data.run.id }); + if ('error' in res) { + // Throw error so that the modal can catch and display it + throw res.error; + } + }} run={runResult.data.run} navigateToRun={navigateToRun} /> diff --git a/ui/apps/dev-server-ui/src/coreapi.ts b/ui/apps/dev-server-ui/src/coreapi.ts index ce1983824b..4acdedb19b 100644 --- a/ui/apps/dev-server-ui/src/coreapi.ts +++ b/ui/apps/dev-server-ui/src/coreapi.ts @@ -246,3 +246,9 @@ export const CANCEL_RUN = gql` } } `; + +export const RERUN = gql` + mutation Rerun($runID: ULID!) { + rerun(runID: $runID) + } +`; diff --git a/ui/apps/dev-server-ui/src/store/generated.ts b/ui/apps/dev-server-ui/src/store/generated.ts index 34be2ecb30..eb4f9b0501 100644 --- a/ui/apps/dev-server-ui/src/store/generated.ts +++ b/ui/apps/dev-server-ui/src/store/generated.ts @@ -216,6 +216,7 @@ export type Mutation = { deleteApp: Scalars['String']; deleteAppByName: Scalars['Boolean']; invokeFunction: Maybe; + rerun: Scalars['ULID']; updateApp: App; }; @@ -246,6 +247,11 @@ export type MutationInvokeFunctionArgs = { }; +export type MutationRerunArgs = { + runID: Scalars['ULID']; +}; + + export type MutationUpdateAppArgs = { input: UpdateAppInput; }; @@ -503,6 +509,13 @@ export type CancelRunMutationVariables = Exact<{ export type CancelRunMutation = { __typename?: 'Mutation', cancelRun: { __typename?: 'FunctionRun', id: string } }; +export type RerunMutationVariables = Exact<{ + runID: Scalars['ULID']; +}>; + + +export type RerunMutation = { __typename?: 'Mutation', rerun: any }; + export const GetEventDocument = ` query GetEvent($id: ID!) { @@ -728,6 +741,11 @@ export const CancelRunDocument = ` } } `; +export const RerunDocument = ` + mutation Rerun($runID: ULID!) { + rerun(runID: $runID) +} + `; const injectedRtkApi = api.injectEndpoints({ endpoints: (build) => ({ @@ -770,9 +788,12 @@ const injectedRtkApi = api.injectEndpoints({ CancelRun: build.mutation({ query: (variables) => ({ document: CancelRunDocument, variables }) }), + Rerun: build.mutation({ + query: (variables) => ({ document: RerunDocument, variables }) + }), }), }); export { injectedRtkApi as api }; -export const { useGetEventQuery, useLazyGetEventQuery, useGetFunctionRunQuery, useLazyGetFunctionRunQuery, useGetFunctionsQuery, useLazyGetFunctionsQuery, useGetAppsQuery, useLazyGetAppsQuery, useCreateAppMutation, useUpdateAppMutation, useDeleteAppMutation, useGetTriggersStreamQuery, useLazyGetTriggersStreamQuery, useGetFunctionRunStatusQuery, useLazyGetFunctionRunStatusQuery, useGetFunctionRunOutputQuery, useLazyGetFunctionRunOutputQuery, useGetHistoryItemOutputQuery, useLazyGetHistoryItemOutputQuery, useInvokeFunctionMutation, useCancelRunMutation } = injectedRtkApi; +export const { useGetEventQuery, useLazyGetEventQuery, useGetFunctionRunQuery, useLazyGetFunctionRunQuery, useGetFunctionsQuery, useLazyGetFunctionsQuery, useGetAppsQuery, useLazyGetAppsQuery, useCreateAppMutation, useUpdateAppMutation, useDeleteAppMutation, useGetTriggersStreamQuery, useLazyGetTriggersStreamQuery, useGetFunctionRunStatusQuery, useLazyGetFunctionRunStatusQuery, useGetFunctionRunOutputQuery, useLazyGetFunctionRunOutputQuery, useGetHistoryItemOutputQuery, useLazyGetHistoryItemOutputQuery, useInvokeFunctionMutation, useCancelRunMutation, useRerunMutation } = injectedRtkApi; diff --git a/ui/packages/components/src/RerunButton.tsx b/ui/packages/components/src/RerunButton.tsx new file mode 100644 index 0000000000..9e78c424a1 --- /dev/null +++ b/ui/packages/components/src/RerunButton.tsx @@ -0,0 +1,36 @@ +import { useState } from 'react'; +import { ArrowPathIcon } from '@heroicons/react/20/solid'; +import { Button } from '@inngest/components/Button'; +import { toast } from 'sonner'; + +import { cn } from './utils/classNames'; + +type Props = { + onClick: () => Promise; +}; + +export function RerunButton(props: Props) { + const [isLoading, setIsLoading] = useState(false); + + async function onClick() { + setIsLoading(true); + try { + await props.onClick(); + toast.success('Queued rerun'); + } catch { + toast.error('Failed to queue rerun'); + } finally { + setIsLoading(false); + } + } + + return ( +