Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions console/src/components/scheduled-detail-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
anchor_at: string | null
interval: string | null
paused_at: string | null
has_pending_events: boolean | null
data: Record<string, unknown> | null
created_at: string
updated_at: string
Expand Down Expand Up @@ -128,19 +129,17 @@
{formatDate(preferences, item.scheduled_at, "PPpp")}
</span>
</DateTimeEdit>
{!item.paused_at &&
item.scheduled_at &&
new Date(item.scheduled_at) <= new Date() && (
{!item.paused_at && item.has_pending_events && (
<Tooltip>

Check warning on line 133 in console/src/components/scheduled-detail-table.tsx

View workflow job for this annotation

GitHub Actions / lint

Delete `····`
<TooltipTrigger asChild>

Check warning on line 134 in console/src/components/scheduled-detail-table.tsx

View workflow job for this annotation

GitHub Actions / lint

Delete `····`
<span className="inline-flex items-center gap-1 text-xs text-blue-600 dark:text-blue-400">

Check warning on line 135 in console/src/components/scheduled-detail-table.tsx

View workflow job for this annotation

GitHub Actions / lint

Delete `····`
<Loader2 className="h-3 w-3 animate-spin" />

Check warning on line 136 in console/src/components/scheduled-detail-table.tsx

View workflow job for this annotation

GitHub Actions / lint

Replace `····················································` with `················································`
</span>

Check warning on line 137 in console/src/components/scheduled-detail-table.tsx

View workflow job for this annotation

GitHub Actions / lint

Delete `····`
</TooltipTrigger>

Check warning on line 138 in console/src/components/scheduled-detail-table.tsx

View workflow job for this annotation

GitHub Actions / lint

Delete `····`
<TooltipContent>

Check warning on line 139 in console/src/components/scheduled-detail-table.tsx

View workflow job for this annotation

GitHub Actions / lint

Delete `····`
{t(

Check warning on line 140 in console/src/components/scheduled-detail-table.tsx

View workflow job for this annotation

GitHub Actions / lint

Delete `····`
"scheduled_sending_tooltip",

Check warning on line 141 in console/src/components/scheduled-detail-table.tsx

View workflow job for this annotation

GitHub Actions / lint

Delete `····`
"This message is scheduled to be sent out. It could take up to 1 minute to process.",

Check warning on line 142 in console/src/components/scheduled-detail-table.tsx

View workflow job for this annotation

GitHub Actions / lint

Delete `····`
)}
</TooltipContent>
</Tooltip>
Expand Down Expand Up @@ -484,10 +483,7 @@
{t("paused", "Paused")}
</Badge>
)}
{!item.paused_at &&
item.scheduled_at &&
new Date(item.scheduled_at) <=
new Date() && (
{!item.paused_at && item.has_pending_events && (
<Tooltip>
<TooltipTrigger asChild>
<Badge
Expand Down
89 changes: 51 additions & 38 deletions internal/http/controllers/v1/management/scheduled.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,17 +269,17 @@ func (srv *ScheduledController) GetUserScheduled(w http.ResponseWriter, r *http.

logger.Info("user schedules listed", zap.Int("total", total), zap.Int("count", len(items)))

results := make([]oapi.UserScheduled, len(items))
results := make([]scheduledResponse, len(items))
for i, item := range items {
results[i] = userScheduleToOAPI(item)
}

json.Write(w, http.StatusOK, oapi.UserScheduledList{
Results: results,
Total: total,
Limit: pagination.Limit,
Offset: pagination.Offset,
})
json.Write(w, http.StatusOK, struct {
Results []scheduledResponse `json:"results"`
Total int `json:"total"`
Limit int `json:"limit"`
Offset int `json:"offset"`
}{Results: results, Total: total, Limit: pagination.Limit, Offset: pagination.Offset})
}

// UpsertUserScheduled creates or updates a scheduled instance for a specific user.
Expand Down Expand Up @@ -548,17 +548,17 @@ func (srv *ScheduledController) GetOrganizationScheduled(w http.ResponseWriter,

logger.Info("organization schedules listed", zap.Int("total", total), zap.Int("count", len(items)))

results := make([]oapi.UserScheduled, len(items))
results := make([]scheduledResponse, len(items))
for i, item := range items {
results[i] = orgScheduleToOAPI(item)
}

json.Write(w, http.StatusOK, oapi.UserScheduledList{
Results: results,
Total: total,
Limit: pagination.Limit,
Offset: pagination.Offset,
})
json.Write(w, http.StatusOK, struct {
Results []scheduledResponse `json:"results"`
Total int `json:"total"`
Limit int `json:"limit"`
Offset int `json:"offset"`
}{Results: results, Total: total, Limit: pagination.Limit, Offset: pagination.Offset})
}

// UpsertOrganizationScheduled creates or updates a scheduled instance for a specific organization.
Expand Down Expand Up @@ -782,19 +782,29 @@ func (srv *ScheduledController) UpdateOrganizationScheduled(w http.ResponseWrite
json.Write(w, http.StatusOK, orgScheduleToOAPI(*updated))
}

// scheduledResponse embeds the generated OAPI type and adds computed fields
// that don't belong in the public spec.
type scheduledResponse struct {
oapi.UserScheduled
HasPendingEvents bool `json:"has_pending_events"`
}

// userScheduleToOAPI converts a store UserSchedule to the OAPI UserScheduled type.
func userScheduleToOAPI(us subjects.UserSchedule) oapi.UserScheduled {
result := oapi.UserScheduled{
Id: us.ID,
UserId: us.UserID,
ScheduledId: us.ScheduleID,
Data: us.Data,
Interval: us.Interval,
StartAt: us.StartAt,
AnchorAt: us.AnchorAt,
PausedAt: us.PausedAt,
CreatedAt: us.CreatedAt,
UpdatedAt: us.UpdatedAt,
func userScheduleToOAPI(us subjects.UserSchedule) scheduledResponse {
result := scheduledResponse{
UserScheduled: oapi.UserScheduled{
Id: us.ID,
UserId: us.UserID,
ScheduledId: us.ScheduleID,
Data: us.Data,
Interval: us.Interval,
StartAt: us.StartAt,
AnchorAt: us.AnchorAt,
PausedAt: us.PausedAt,
CreatedAt: us.CreatedAt,
UpdatedAt: us.UpdatedAt,
},
HasPendingEvents: us.HasPendingEvents,
}
if us.ScheduledAt != nil {
result.ScheduledAt = *us.ScheduledAt
Expand All @@ -808,18 +818,21 @@ func userScheduleToOAPI(us subjects.UserSchedule) oapi.UserScheduled {

// orgScheduleToOAPI converts a store OrganizationSchedule to the OAPI UserScheduled type.
// Reuses UserScheduled since the wire format is the same; UserId carries OrganizationID.
func orgScheduleToOAPI(os subjects.OrganizationSchedule) oapi.UserScheduled {
result := oapi.UserScheduled{
Id: os.ID,
UserId: os.OrganizationID,
ScheduledId: os.ScheduleID,
Data: os.Data,
Interval: os.Interval,
StartAt: os.StartAt,
AnchorAt: os.AnchorAt,
PausedAt: os.PausedAt,
CreatedAt: os.CreatedAt,
UpdatedAt: os.UpdatedAt,
func orgScheduleToOAPI(os subjects.OrganizationSchedule) scheduledResponse {
result := scheduledResponse{
UserScheduled: oapi.UserScheduled{
Id: os.ID,
UserId: os.OrganizationID,
ScheduledId: os.ScheduleID,
Data: os.Data,
Interval: os.Interval,
StartAt: os.StartAt,
AnchorAt: os.AnchorAt,
PausedAt: os.PausedAt,
CreatedAt: os.CreatedAt,
UpdatedAt: os.UpdatedAt,
},
HasPendingEvents: os.HasPendingEvents,
}
if os.ScheduledAt != nil {
result.ScheduledAt = *os.ScheduledAt
Expand Down
58 changes: 34 additions & 24 deletions internal/store/subjects/scheduled.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,19 @@ type ScheduleOffset struct {

// UserSchedule represents a user's assignment to a schedule.
type UserSchedule struct {
ID uuid.UUID `db:"id" json:"id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
ScheduleID uuid.UUID `db:"schedule_id" json:"schedule_id"`
ScheduledAt *time.Time `db:"scheduled_at" json:"scheduled_at,omitempty"` // fire time for single
StartAt *time.Time `db:"start_at" json:"start_at,omitempty"` // start of interval for recurring (historical, never mutated after creation)
AnchorAt *time.Time `db:"anchor_at" json:"anchor_at,omitempty"` // computation base for occurrence (scheduled_at = anchor_at + occurrence * interval)
Interval *string `db:"interval" json:"interval,omitempty"` // duration string for recurring (e.g. "1 month", "30 days")
Occurrence int `db:"occurrence" json:"occurrence"` // number of intervals advanced from anchor_at
Data json.RawMessage `db:"data" json:"data"`
PausedAt *time.Time `db:"paused_at" json:"paused_at,omitempty"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
ID uuid.UUID `db:"id" json:"id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
ScheduleID uuid.UUID `db:"schedule_id" json:"schedule_id"`
ScheduledAt *time.Time `db:"scheduled_at" json:"scheduled_at,omitempty"` // fire time for single
StartAt *time.Time `db:"start_at" json:"start_at,omitempty"` // start of interval for recurring (historical, never mutated after creation)
AnchorAt *time.Time `db:"anchor_at" json:"anchor_at,omitempty"` // computation base for occurrence (scheduled_at = anchor_at + occurrence * interval)
Interval *string `db:"interval" json:"interval,omitempty"` // duration string for recurring (e.g. "1 month", "30 days")
Occurrence int `db:"occurrence" json:"occurrence"` // number of intervals advanced from anchor_at
Data json.RawMessage `db:"data" json:"data"`
PausedAt *time.Time `db:"paused_at" json:"paused_at,omitempty"`
HasPendingEvents bool `db:"has_pending_events" json:"has_pending_events"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}

// ScheduledEvent represents a pre-computed event to be fired by the scheduler.
Expand Down Expand Up @@ -89,18 +90,19 @@ type DueScheduledEvent struct {

// OrganizationSchedule represents an organization's assignment to a schedule.
type OrganizationSchedule struct {
ID uuid.UUID `db:"id" json:"id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
ScheduleID uuid.UUID `db:"schedule_id" json:"schedule_id"`
ScheduledAt *time.Time `db:"scheduled_at" json:"scheduled_at,omitempty"`
StartAt *time.Time `db:"start_at" json:"start_at,omitempty"`
AnchorAt *time.Time `db:"anchor_at" json:"anchor_at,omitempty"` // computation base for occurrence (scheduled_at = anchor_at + occurrence * interval)
Interval *string `db:"interval" json:"interval,omitempty"`
Occurrence int `db:"occurrence" json:"occurrence"` // number of intervals advanced from anchor_at
Data json.RawMessage `db:"data" json:"data"`
PausedAt *time.Time `db:"paused_at" json:"paused_at,omitempty"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
ID uuid.UUID `db:"id" json:"id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
ScheduleID uuid.UUID `db:"schedule_id" json:"schedule_id"`
ScheduledAt *time.Time `db:"scheduled_at" json:"scheduled_at,omitempty"`
StartAt *time.Time `db:"start_at" json:"start_at,omitempty"`
AnchorAt *time.Time `db:"anchor_at" json:"anchor_at,omitempty"` // computation base for occurrence (scheduled_at = anchor_at + occurrence * interval)
Interval *string `db:"interval" json:"interval,omitempty"`
Occurrence int `db:"occurrence" json:"occurrence"` // number of intervals advanced from anchor_at
Data json.RawMessage `db:"data" json:"data"`
PausedAt *time.Time `db:"paused_at" json:"paused_at,omitempty"`
HasPendingEvents bool `db:"has_pending_events" json:"has_pending_events"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}

// OrgScheduledEvent represents a pre-computed event for an organization schedule.
Expand Down Expand Up @@ -677,6 +679,10 @@ func (s *ScheduledStore) ListUserSchedules(ctx context.Context, projectID, userI
SELECT
us.id, us.user_id, us.schedule_id, us.scheduled_at, us.start_at, us.anchor_at, us.interval,
us.occurrence, COALESCE(us.data, '{}'::jsonb) AS data, us.paused_at, us.created_at, us.updated_at,
EXISTS (
SELECT 1 FROM user_scheduled_events use
WHERE use.user_schedule_id = us.id AND use.fired_at IS NULL
) AS has_pending_events,
COUNT(*) OVER () AS total_count
FROM user_schedules us
INNER JOIN schedules sc ON us.schedule_id = sc.id
Expand Down Expand Up @@ -1280,6 +1286,10 @@ func (s *ScheduledStore) ListOrganizationSchedules(ctx context.Context, projectI
SELECT
os.id, os.organization_id, os.schedule_id, os.scheduled_at, os.start_at, os.anchor_at, os.interval,
os.occurrence, COALESCE(os.data, '{}'::jsonb) AS data, os.paused_at, os.created_at, os.updated_at,
EXISTS (
SELECT 1 FROM organization_scheduled_events ose
WHERE ose.organization_schedule_id = os.id AND ose.fired_at IS NULL
) AS has_pending_events,
COUNT(*) OVER () AS total_count
FROM organization_schedules os
INNER JOIN schedules sc ON os.schedule_id = sc.id
Expand Down
Loading