feat(releases): replay actions (#932)

* feat(releases): replay actions

* feat(releases): replay actions component

* fix: update filter actions

* fix: select filter_id from ras
This commit is contained in:
ze0s 2023-05-15 21:30:04 +02:00 committed by GitHub
parent 97333d334f
commit 6898ad8315
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 752 additions and 189 deletions

View file

@ -99,7 +99,7 @@ func (s *service) RunAction(ctx context.Context, action *domain.Action, release
payload := &domain.NotificationPayload{
Event: domain.NotificationEventPushApproved,
ReleaseName: release.TorrentName,
Filter: release.Filter.Name,
Filter: release.FilterName,
Indexer: release.Indexer,
InfoHash: release.TorrentHash,
Size: release.Size,

View file

@ -19,7 +19,8 @@ import (
type Service interface {
Store(ctx context.Context, action domain.Action) (*domain.Action, error)
List(ctx context.Context) ([]domain.Action, error)
Delete(actionID int) error
Get(ctx context.Context, req *domain.GetActionRequest) (*domain.Action, error)
Delete(ctx context.Context, req *domain.DeleteActionRequest) error
DeleteByFilterID(ctx context.Context, filterID int) error
ToggleEnabled(actionID int) error
@ -51,18 +52,37 @@ func (s *service) Store(ctx context.Context, action domain.Action) (*domain.Acti
return s.repo.Store(ctx, action)
}
func (s *service) Delete(actionID int) error {
return s.repo.Delete(actionID)
func (s *service) List(ctx context.Context) ([]domain.Action, error) {
return s.repo.List(ctx)
}
func (s *service) Get(ctx context.Context, req *domain.GetActionRequest) (*domain.Action, error) {
a, err := s.repo.Get(ctx, req)
if err != nil {
return nil, err
}
// optionally attach download client to action
if a.ClientID > 0 {
client, err := s.clientSvc.FindByID(ctx, a.ClientID)
if err != nil {
return nil, err
}
a.Client = client
}
return a, nil
}
func (s *service) Delete(ctx context.Context, req *domain.DeleteActionRequest) error {
return s.repo.Delete(ctx, req)
}
func (s *service) DeleteByFilterID(ctx context.Context, filterID int) error {
return s.repo.DeleteByFilterID(ctx, filterID)
}
func (s *service) List(ctx context.Context) ([]domain.Action, error) {
return s.repo.List(ctx)
}
func (s *service) ToggleEnabled(actionID int) error {
return s.repo.ToggleEnabled(actionID)
}

View file

@ -280,30 +280,117 @@ func (r *ActionRepo) List(ctx context.Context) ([]domain.Action, error) {
a.ClientID = clientID.Int32
actions = append(actions, a)
}
if err := rows.Err(); err != nil {
return nil, errors.Wrap(err, "rows error")
if err := rows.Err(); err != nil {
return nil, errors.Wrap(err, "rows error")
}
}
return actions, nil
}
func (r *ActionRepo) Delete(actionID int) error {
func (r *ActionRepo) Get(ctx context.Context, req *domain.GetActionRequest) (*domain.Action, error) {
queryBuilder := r.db.squirrel.
Select(
"id",
"name",
"type",
"enabled",
"exec_cmd",
"exec_args",
"watch_folder",
"category",
"tags",
"label",
"save_path",
"paused",
"ignore_rules",
"limit_download_speed",
"limit_upload_speed",
"limit_ratio",
"limit_seed_time",
"reannounce_skip",
"reannounce_delete",
"reannounce_interval",
"reannounce_max_attempts",
"webhook_host",
"webhook_type",
"webhook_method",
"webhook_data",
"client_id",
"filter_id",
).
From("action").
Where(sq.Eq{"id": req.Id})
query, args, err := queryBuilder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "error building query")
}
row := r.db.handler.QueryRowContext(ctx, query, args...)
if err != nil {
return nil, errors.Wrap(err, "error executing query")
}
if err := row.Err(); err != nil {
return nil, errors.Wrap(err, "rows error")
}
var a domain.Action
var execCmd, execArgs, watchFolder, category, tags, label, savePath, webhookHost, webhookType, webhookMethod, webhookData sql.NullString
var limitUl, limitDl, limitSeedTime sql.NullInt64
var limitRatio sql.NullFloat64
var clientID, filterID sql.NullInt32
var paused, ignoreRules sql.NullBool
if err := row.Scan(&a.ID, &a.Name, &a.Type, &a.Enabled, &execCmd, &execArgs, &watchFolder, &category, &tags, &label, &savePath, &paused, &ignoreRules, &limitDl, &limitUl, &limitRatio, &limitSeedTime, &a.ReAnnounceSkip, &a.ReAnnounceDelete, &a.ReAnnounceInterval, &a.ReAnnounceMaxAttempts, &webhookHost, &webhookType, &webhookMethod, &webhookData, &clientID, &filterID); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, errors.Wrap(err, "error scanning row")
}
a.Category = category.String
a.Tags = tags.String
a.Label = label.String
a.SavePath = savePath.String
a.Paused = paused.Bool
a.IgnoreRules = ignoreRules.Bool
a.LimitDownloadSpeed = limitDl.Int64
a.LimitUploadSpeed = limitUl.Int64
a.LimitRatio = limitRatio.Float64
a.LimitSeedTime = limitSeedTime.Int64
a.WebhookHost = webhookHost.String
a.WebhookType = webhookType.String
a.WebhookMethod = webhookMethod.String
a.WebhookData = webhookData.String
a.ClientID = clientID.Int32
a.FilterID = int(filterID.Int32)
return &a, nil
}
func (r *ActionRepo) Delete(ctx context.Context, req *domain.DeleteActionRequest) error {
queryBuilder := r.db.squirrel.
Delete("action").
Where(sq.Eq{"id": actionID})
Where(sq.Eq{"id": req.ActionId})
query, args, err := queryBuilder.ToSql()
if err != nil {
return errors.Wrap(err, "error building query")
}
_, err = r.db.handler.Exec(query, args...)
if err != nil {
if _, err = r.db.handler.ExecContext(ctx, query, args...); err != nil {
return errors.Wrap(err, "error executing query")
}
r.log.Debug().Msgf("action.delete: %v", actionID)
r.log.Debug().Msgf("action.delete: %v", req.ActionId)
return nil
}
@ -506,113 +593,171 @@ func (r *ActionRepo) StoreFilterActions(ctx context.Context, actions []*domain.A
defer tx.Rollback()
deleteQueryBuilder := r.db.squirrel.
Delete("action").
Where(sq.Eq{"filter_id": filterID})
deleteQuery, deleteArgs, err := deleteQueryBuilder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "error building query")
}
_, err = tx.ExecContext(ctx, deleteQuery, deleteArgs...)
if err != nil {
return nil, errors.Wrap(err, "error executing query")
}
for _, action := range actions {
execCmd := toNullString(action.ExecCmd)
execArgs := toNullString(action.ExecArgs)
watchFolder := toNullString(action.WatchFolder)
category := toNullString(action.Category)
tags := toNullString(action.Tags)
label := toNullString(action.Label)
savePath := toNullString(action.SavePath)
contentLayout := toNullString(string(action.ContentLayout))
webhookHost := toNullString(action.WebhookHost)
webhookType := toNullString(action.WebhookType)
webhookMethod := toNullString(action.WebhookMethod)
webhookData := toNullString(action.WebhookData)
action := action
limitDL := toNullInt64(action.LimitDownloadSpeed)
limitUL := toNullInt64(action.LimitUploadSpeed)
limitRatio := toNullFloat64(action.LimitRatio)
limitSeedTime := toNullInt64(action.LimitSeedTime)
clientID := toNullInt32(action.ClientID)
if action.ID > 0 {
execCmd := toNullString(action.ExecCmd)
execArgs := toNullString(action.ExecArgs)
watchFolder := toNullString(action.WatchFolder)
category := toNullString(action.Category)
tags := toNullString(action.Tags)
label := toNullString(action.Label)
savePath := toNullString(action.SavePath)
contentLayout := toNullString(string(action.ContentLayout))
webhookHost := toNullString(action.WebhookHost)
webhookType := toNullString(action.WebhookType)
webhookMethod := toNullString(action.WebhookMethod)
webhookData := toNullString(action.WebhookData)
queryBuilder := r.db.squirrel.
Insert("action").
Columns(
"name",
"type",
"enabled",
"exec_cmd",
"exec_args",
"watch_folder",
"category",
"tags",
"label",
"save_path",
"paused",
"ignore_rules",
"skip_hash_check",
"content_layout",
"limit_upload_speed",
"limit_download_speed",
"limit_ratio",
"limit_seed_time",
"reannounce_skip",
"reannounce_delete",
"reannounce_interval",
"reannounce_max_attempts",
"webhook_host",
"webhook_type",
"webhook_method",
"webhook_data",
"client_id",
"filter_id",
).
Values(
action.Name,
action.Type,
action.Enabled,
execCmd,
execArgs,
watchFolder,
category,
tags,
label,
savePath,
action.Paused,
action.IgnoreRules,
action.SkipHashCheck,
contentLayout,
limitUL,
limitDL,
limitRatio,
limitSeedTime,
action.ReAnnounceSkip,
action.ReAnnounceDelete,
action.ReAnnounceInterval,
action.ReAnnounceMaxAttempts,
webhookHost,
webhookType,
webhookMethod,
webhookData,
clientID,
filterID,
).
Suffix("RETURNING id").RunWith(tx)
limitDL := toNullInt64(action.LimitDownloadSpeed)
limitUL := toNullInt64(action.LimitUploadSpeed)
limitRatio := toNullFloat64(action.LimitRatio)
limitSeedTime := toNullInt64(action.LimitSeedTime)
// return values
var retID int
clientID := toNullInt32(action.ClientID)
err = queryBuilder.QueryRowContext(ctx).Scan(&retID)
if err != nil {
return nil, errors.Wrap(err, "error executing query")
var err error
queryBuilder := r.db.squirrel.
Update("action").
Set("name", action.Name).
Set("type", action.Type).
Set("enabled", action.Enabled).
Set("exec_cmd", execCmd).
Set("exec_args", execArgs).
Set("watch_folder", watchFolder).
Set("category", category).
Set("tags", tags).
Set("label", label).
Set("save_path", savePath).
Set("paused", action.Paused).
Set("ignore_rules", action.IgnoreRules).
Set("skip_hash_check", action.SkipHashCheck).
Set("content_layout", contentLayout).
Set("limit_upload_speed", limitUL).
Set("limit_download_speed", limitDL).
Set("limit_ratio", limitRatio).
Set("limit_seed_time", limitSeedTime).
Set("reannounce_skip", action.ReAnnounceSkip).
Set("reannounce_delete", action.ReAnnounceDelete).
Set("reannounce_interval", action.ReAnnounceInterval).
Set("reannounce_max_attempts", action.ReAnnounceMaxAttempts).
Set("webhook_host", webhookHost).
Set("webhook_type", webhookType).
Set("webhook_method", webhookMethod).
Set("webhook_data", webhookData).
Set("client_id", clientID).
Set("filter_id", filterID).
Where(sq.Eq{"id": action.ID})
query, args, err := queryBuilder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "error building query")
}
if _, err = tx.ExecContext(ctx, query, args...); err != nil {
return nil, errors.Wrap(err, "error executing query")
}
r.log.Trace().Msgf("action.StoreFilterActions: update %d", action.ID)
} else {
execCmd := toNullString(action.ExecCmd)
execArgs := toNullString(action.ExecArgs)
watchFolder := toNullString(action.WatchFolder)
category := toNullString(action.Category)
tags := toNullString(action.Tags)
label := toNullString(action.Label)
savePath := toNullString(action.SavePath)
contentLayout := toNullString(string(action.ContentLayout))
webhookHost := toNullString(action.WebhookHost)
webhookType := toNullString(action.WebhookType)
webhookMethod := toNullString(action.WebhookMethod)
webhookData := toNullString(action.WebhookData)
limitDL := toNullInt64(action.LimitDownloadSpeed)
limitUL := toNullInt64(action.LimitUploadSpeed)
limitRatio := toNullFloat64(action.LimitRatio)
limitSeedTime := toNullInt64(action.LimitSeedTime)
clientID := toNullInt32(action.ClientID)
queryBuilder := r.db.squirrel.
Insert("action").
Columns(
"name",
"type",
"enabled",
"exec_cmd",
"exec_args",
"watch_folder",
"category",
"tags",
"label",
"save_path",
"paused",
"ignore_rules",
"skip_hash_check",
"content_layout",
"limit_upload_speed",
"limit_download_speed",
"limit_ratio",
"limit_seed_time",
"reannounce_skip",
"reannounce_delete",
"reannounce_interval",
"reannounce_max_attempts",
"webhook_host",
"webhook_type",
"webhook_method",
"webhook_data",
"client_id",
"filter_id",
).
Values(
action.Name,
action.Type,
action.Enabled,
execCmd,
execArgs,
watchFolder,
category,
tags,
label,
savePath,
action.Paused,
action.IgnoreRules,
action.SkipHashCheck,
contentLayout,
limitUL,
limitDL,
limitRatio,
limitSeedTime,
action.ReAnnounceSkip,
action.ReAnnounceDelete,
action.ReAnnounceInterval,
action.ReAnnounceMaxAttempts,
webhookHost,
webhookType,
webhookMethod,
webhookData,
clientID,
filterID,
).
Suffix("RETURNING id").RunWith(tx)
// return values
var retID int
if err = queryBuilder.QueryRowContext(ctx).Scan(&retID); err != nil {
return nil, errors.Wrap(err, "error executing query")
}
action.ID = retID
r.log.Trace().Msgf("action.StoreFilterActions: store %d", action.ID)
}
action.ID = retID
r.log.Debug().Msgf("action.StoreFilterActions: store '%v' type: '%v' on filter: %v", action.Name, action.Type, filterID)
}

View file

@ -271,6 +271,7 @@ CREATE TABLE release_action_status
id SERIAL PRIMARY KEY,
status TEXT,
action TEXT NOT NULL,
action_id INTEGER,
type TEXT NOT NULL,
client TEXT,
filter TEXT,
@ -280,6 +281,7 @@ CREATE TABLE release_action_status
raw TEXT,
log TEXT,
release_id INTEGER NOT NULL,
FOREIGN KEY (action_id) REFERENCES "action"(id),
FOREIGN KEY (release_id) REFERENCES "release"(id) ON DELETE CASCADE,
FOREIGN KEY (filter_id) REFERENCES "filter"(id) ON DELETE SET NULL
);
@ -691,4 +693,10 @@ ADD COLUMN topic text;`,
ALTER TABLE filter
ADD COLUMN use_regex_description BOOLEAN DEFAULT FALSE;`,
`ALTER TABLE release_action_status
ADD action_id INTEGER;
ALTER TABLE release_action_status
ADD CONSTRAINT release_action_status_action_id_fk
FOREIGN KEY (action_id) REFERENCES action;`,
}

View file

@ -79,8 +79,8 @@ func (repo *ReleaseRepo) StoreReleaseActionStatus(ctx context.Context, status *d
} else {
queryBuilder := repo.db.squirrel.
Insert("release_action_status").
Columns("status", "action", "type", "client", "filter", "filter_id", "rejections", "timestamp", "release_id").
Values(status.Status, status.Action, status.Type, status.Client, status.Filter, status.FilterID, pq.Array(status.Rejections), status.Timestamp.Format(time.RFC3339), status.ReleaseID).
Columns("status", "action", "action_id", "type", "client", "filter", "filter_id", "rejections", "timestamp", "release_id").
Values(status.Status, status.Action, status.ActionID, status.Type, status.Client, status.Filter, status.FilterID, pq.Array(status.Rejections), status.Timestamp.Format(time.RFC3339), status.ReleaseID).
Suffix("RETURNING id").RunWith(repo.db.handler)
// return values
@ -207,7 +207,7 @@ func (repo *ReleaseRepo) findReleases(ctx context.Context, tx *Tx, params domain
queryBuilder := repo.db.squirrel.
Select("r.id", "r.filter_status", "r.rejections", "r.indexer", "r.filter", "r.protocol", "r.info_url", "r.download_url", "r.title", "r.torrent_name", "r.size", "r.timestamp",
"ras.id", "ras.status", "ras.action", "ras.type", "ras.client", "ras.filter", "ras.rejections", "ras.timestamp").
"ras.id", "ras.status", "ras.action", "ras.action_id", "ras.type", "ras.client", "ras.filter", "ras.filter_id", "ras.release_id", "ras.rejections", "ras.timestamp").
Column(sq.Alias(countQuery, "page_total")).
From("release r").
OrderBy("r.id DESC").
@ -242,22 +242,25 @@ func (repo *ReleaseRepo) findReleases(ctx context.Context, tx *Tx, params domain
var rlsindexer, rlsfilter, infoUrl, downloadUrl sql.NullString
var rasId sql.NullInt64
var rasId, rasFilterId, rasReleaseId, rasActionId sql.NullInt64
var rasStatus, rasAction, rasType, rasClient, rasFilter sql.NullString
var rasRejections []sql.NullString
var rasTimestamp sql.NullTime
if err := rows.Scan(&rls.ID, &rls.FilterStatus, pq.Array(&rls.Rejections), &rlsindexer, &rlsfilter, &rls.Protocol, &infoUrl, &downloadUrl, &rls.Title, &rls.TorrentName, &rls.Size, &rls.Timestamp, &rasId, &rasStatus, &rasAction, &rasType, &rasClient, &rasFilter, pq.Array(&rasRejections), &rasTimestamp, &countItems); err != nil {
if err := rows.Scan(&rls.ID, &rls.FilterStatus, pq.Array(&rls.Rejections), &rlsindexer, &rlsfilter, &rls.Protocol, &infoUrl, &downloadUrl, &rls.Title, &rls.TorrentName, &rls.Size, &rls.Timestamp, &rasId, &rasStatus, &rasAction, &rasActionId, &rasType, &rasClient, &rasFilter, &rasFilterId, &rasReleaseId, pq.Array(&rasRejections), &rasTimestamp, &countItems); err != nil {
return res, 0, 0, errors.Wrap(err, "error scanning row")
}
ras.ID = rasId.Int64
ras.Status = domain.ReleasePushStatus(rasStatus.String)
ras.Action = rasAction.String
ras.ActionID = rasActionId.Int64
ras.Type = domain.ActionType(rasType.String)
ras.Client = rasClient.String
ras.Filter = rasFilter.String
ras.FilterID = rasFilterId.Int64
ras.Timestamp = rasTimestamp.Time
ras.ReleaseID = rasReleaseId.Int64
ras.Rejections = []string{}
for _, rejection := range rasRejections {
@ -351,7 +354,7 @@ func (repo *ReleaseRepo) GetIndexerOptions(ctx context.Context) ([]string, error
func (repo *ReleaseRepo) GetActionStatusByReleaseID(ctx context.Context, releaseID int64) ([]domain.ReleaseActionStatus, error) {
queryBuilder := repo.db.squirrel.
Select("id", "status", "action", "type", "client", "filter", "rejections", "timestamp").
Select("id", "status", "action", "action_id", "type", "client", "filter", "release_id", "rejections", "timestamp").
From("release_action_status").
Where(sq.Eq{"release_id": releaseID})
@ -378,11 +381,13 @@ func (repo *ReleaseRepo) GetActionStatusByReleaseID(ctx context.Context, release
var rls domain.ReleaseActionStatus
var client, filter sql.NullString
var actionId sql.NullInt64
if err := rows.Scan(&rls.ID, &rls.Status, &rls.Action, &rls.Type, &client, &filter, pq.Array(&rls.Rejections), &rls.Timestamp); err != nil {
if err := rows.Scan(&rls.ID, &rls.Status, &rls.Action, &actionId, &rls.Type, &client, &filter, &rls.ReleaseID, pq.Array(&rls.Rejections), &rls.Timestamp); err != nil {
return res, errors.Wrap(err, "error scanning row")
}
rls.ActionID = actionId.Int64
rls.Client = client.String
rls.Filter = filter.String
@ -392,9 +397,96 @@ func (repo *ReleaseRepo) GetActionStatusByReleaseID(ctx context.Context, release
return res, nil
}
func (repo *ReleaseRepo) Get(ctx context.Context, req *domain.GetReleaseRequest) (*domain.Release, error) {
queryBuilder := repo.db.squirrel.
Select("r.id", "r.filter_status", "r.rejections", "r.indexer", "r.filter", "r.filter_id", "r.protocol", "r.info_url", "r.download_url", "r.title", "r.torrent_name", "r.size", "r.timestamp").
From("release r").
OrderBy("r.id DESC").
Where(sq.Eq{"r.id": req.Id})
query, args, err := queryBuilder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "error building query")
}
repo.log.Trace().Str("database", "release.find").Msgf("query: '%v', args: '%v'", query, args)
row := repo.db.handler.QueryRowContext(ctx, query, args...)
if err != nil {
return nil, errors.Wrap(err, "error executing query")
}
if err := row.Err(); err != nil {
return nil, errors.Wrap(err, "error rows find release")
}
var rls domain.Release
var indexerName, filterName, infoUrl, downloadUrl sql.NullString
var filterId sql.NullInt64
if err := row.Scan(&rls.ID, &rls.FilterStatus, pq.Array(&rls.Rejections), &indexerName, &filterName, &filterId, &rls.Protocol, &infoUrl, &downloadUrl, &rls.Title, &rls.TorrentName, &rls.Size, &rls.Timestamp); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, errors.Wrap(err, "error scanning row")
}
rls.Indexer = indexerName.String
rls.FilterName = filterName.String
rls.FilterID = int(filterId.Int64)
rls.ActionStatus = make([]domain.ReleaseActionStatus, 0)
rls.InfoURL = infoUrl.String
rls.TorrentURL = downloadUrl.String
return &rls, nil
}
func (repo *ReleaseRepo) GetActionStatus(ctx context.Context, req *domain.GetReleaseActionStatusRequest) (*domain.ReleaseActionStatus, error) {
queryBuilder := repo.db.squirrel.
Select("id", "status", "action", "action_id", "type", "client", "filter", "filter_id", "release_id", "rejections", "timestamp").
From("release_action_status").
Where(sq.Eq{"id": req.Id})
query, args, err := queryBuilder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "error building query")
}
row := repo.db.handler.QueryRowContext(ctx, query, args...)
if err != nil {
return nil, errors.Wrap(err, "error executing query")
}
if err := row.Err(); err != nil {
repo.log.Error().Stack().Err(err)
return nil, err
}
var rls domain.ReleaseActionStatus
var client, filter sql.NullString
var actionId, filterId sql.NullInt64
if err := row.Scan(&rls.ID, &rls.Status, &rls.Action, &actionId, &rls.Type, &client, &filter, &filterId, &rls.ReleaseID, pq.Array(&rls.Rejections), &rls.Timestamp); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, errors.Wrap(err, "error scanning row")
}
rls.ActionID = actionId.Int64
rls.Client = client.String
rls.Filter = filter.String
rls.FilterID = filterId.Int64
return &rls, nil
}
func (repo *ReleaseRepo) attachActionStatus(ctx context.Context, tx *Tx, releaseID int64) ([]domain.ReleaseActionStatus, error) {
queryBuilder := repo.db.squirrel.
Select("id", "status", "action", "type", "client", "filter", "filter_id", "rejections", "timestamp").
Select("id", "status", "action", "action_id", "type", "client", "filter", "filter_id", "release_id", "rejections", "timestamp").
From("release_action_status").
Where(sq.Eq{"release_id": releaseID})
@ -420,12 +512,13 @@ func (repo *ReleaseRepo) attachActionStatus(ctx context.Context, tx *Tx, release
var rls domain.ReleaseActionStatus
var client, filter sql.NullString
var filterID sql.NullInt64
var actionId, filterID sql.NullInt64
if err := rows.Scan(&rls.ID, &rls.Status, &rls.Action, &rls.Type, &client, &filter, &filterID, pq.Array(&rls.Rejections), &rls.Timestamp); err != nil {
if err := rows.Scan(&rls.ID, &rls.Status, &rls.Action, &actionId, &rls.Type, &client, &filter, &filterID, &rls.ReleaseID, pq.Array(&rls.Rejections), &rls.Timestamp); err != nil {
return res, errors.Wrap(err, "error scanning row")
}
rls.ActionID = actionId.Int64
rls.Client = client.String
rls.Filter = filter.String
rls.FilterID = filterID.Int64

View file

@ -254,6 +254,9 @@ CREATE TABLE release_action_status
id INTEGER PRIMARY KEY,
status TEXT,
action TEXT NOT NULL,
action_id INTEGER
CONSTRAINT release_action_status_action_id_fk
REFERENCES action,
type TEXT NOT NULL,
client TEXT,
filter TEXT,
@ -1084,4 +1087,58 @@ ADD COLUMN topic text;`,
ALTER TABLE filter
ADD COLUMN use_regex_description BOOLEAN DEFAULT FALSE;`,
`create table release_action_status_dg_tmp
(
id INTEGER
primary key,
status TEXT,
action TEXT not null,
action_id INTEGER
constraint release_action_status_action_id_fk
references action,
type TEXT not null,
rejections TEXT default '{}' not null,
timestamp TIMESTAMP default CURRENT_TIMESTAMP,
raw TEXT,
log TEXT,
release_id INTEGER not null
constraint release_action_status_release_id_fkey
references "release"
on delete cascade,
client TEXT,
filter TEXT,
filter_id INTEGER
constraint release_action_status_filter_id_fk
references filter
);
insert into release_action_status_dg_tmp(id, status, action, type, rejections, timestamp, raw, log, release_id, client,
filter, filter_id)
select id,
status,
action,
type,
rejections,
timestamp,
raw,
log,
release_id,
client,
filter,
filter_id
from release_action_status;
drop table release_action_status;
alter table release_action_status_dg_tmp
rename to release_action_status;
create index release_action_status_filter_id_index
on release_action_status (filter_id);
create index release_action_status_release_id_index
on release_action_status (release_id);
create index release_action_status_status_index
on release_action_status (status);`,
}

View file

@ -14,10 +14,11 @@ import (
type ActionRepo interface {
Store(ctx context.Context, action Action) (*Action, error)
StoreFilterActions(ctx context.Context, actions []*Action, filterID int64) ([]*Action, error)
DeleteByFilterID(ctx context.Context, filterID int) error
FindByFilterID(ctx context.Context, filterID int) ([]*Action, error)
List(ctx context.Context) ([]Action, error)
Delete(actionID int) error
Get(ctx context.Context, req *GetActionRequest) (*Action, error)
Delete(ctx context.Context, req *DeleteActionRequest) error
DeleteByFilterID(ctx context.Context, filterID int) error
ToggleEnabled(actionID int) error
}
@ -125,3 +126,11 @@ const (
ActionContentLayoutSubfolderNone ActionContentLayout = "SUBFOLDER_NONE"
ActionContentLayoutSubfolderCreate ActionContentLayout = "SUBFOLDER_CREATE"
)
type GetActionRequest struct {
Id int
}
type DeleteActionRequest struct {
ActionId int
}

View file

@ -30,12 +30,14 @@ type ReleaseRepo interface {
Store(ctx context.Context, release *Release) (*Release, error)
Find(ctx context.Context, params ReleaseQueryParams) (res []*Release, nextCursor int64, count int64, err error)
FindRecent(ctx context.Context) ([]*Release, error)
Get(ctx context.Context, req *GetReleaseRequest) (*Release, error)
GetIndexerOptions(ctx context.Context) ([]string, error)
GetActionStatusByReleaseID(ctx context.Context, releaseID int64) ([]ReleaseActionStatus, error)
Stats(ctx context.Context) (*ReleaseStats, error)
StoreReleaseActionStatus(ctx context.Context, status *ReleaseActionStatus) error
Delete(ctx context.Context) error
CanDownloadShow(ctx context.Context, title string, season int, episode int) (bool, error)
GetActionStatus(ctx context.Context, req *GetReleaseActionStatusRequest) (*ReleaseActionStatus, error)
StoreReleaseActionStatus(ctx context.Context, status *ReleaseActionStatus) error
}
type Release struct {
@ -100,13 +102,14 @@ type ReleaseActionStatus struct {
ID int64 `json:"id"`
Status ReleasePushStatus `json:"status"`
Action string `json:"action"`
ActionID int64 `json:"action_id"`
Type ActionType `json:"type"`
Client string `json:"client"`
Filter string `json:"filter"`
FilterID int64 `json:"-"`
FilterID int64 `json:"filter_id"`
Rejections []string `json:"rejections"`
ReleaseID int64 `json:"release_id"`
Timestamp time.Time `json:"timestamp"`
ReleaseID int64 `json:"-"`
}
func NewReleaseActionStatus(action *Action, release *Release) *ReleaseActionStatus {
@ -114,9 +117,10 @@ func NewReleaseActionStatus(action *Action, release *Release) *ReleaseActionStat
ID: 0,
Status: ReleasePushStatusPending,
Action: action.Name,
ActionID: int64(action.ID),
Type: action.Type,
Filter: release.Filter.Name,
FilterID: int64(release.Filter.ID),
Filter: release.FilterName,
FilterID: int64(release.FilterID),
Rejections: []string{},
Timestamp: time.Now(),
ReleaseID: release.ID,
@ -153,6 +157,8 @@ const (
func (r ReleasePushStatus) String() string {
switch r {
case ReleasePushStatusPending:
return "Pending"
case ReleasePushStatusApproved:
return "Approved"
case ReleasePushStatusRejected:
@ -227,6 +233,20 @@ type ReleaseQueryParams struct {
Search string
}
type ReleaseActionRetryReq struct {
ReleaseId int
ActionStatusId int
ActionId int
}
type GetReleaseRequest struct {
Id int
}
type GetReleaseActionStatusRequest struct {
Id int
}
func NewRelease(indexer string) *Release {
r := &Release{
Indexer: indexer,

View file

@ -11,13 +11,14 @@ import (
"strconv"
"github.com/autobrr/autobrr/internal/domain"
"github.com/go-chi/chi/v5"
)
type actionService interface {
List(ctx context.Context) ([]domain.Action, error)
Store(ctx context.Context, action domain.Action) (*domain.Action, error)
Delete(actionID int) error
Delete(ctx context.Context, req *domain.DeleteActionRequest) error
ToggleEnabled(actionID int) error
}
@ -36,15 +37,19 @@ func newActionHandler(encoder encoder, service actionService) *actionHandler {
func (h actionHandler) Routes(r chi.Router) {
r.Get("/", h.getActions)
r.Post("/", h.storeAction)
r.Delete("/{id}", h.deleteAction)
r.Put("/{id}", h.updateAction)
r.Patch("/{id}/toggleEnabled", h.toggleActionEnabled)
r.Route("/{id}", func(r chi.Router) {
r.Delete("/", h.deleteAction)
r.Put("/", h.updateAction)
r.Patch("/toggleEnabled", h.toggleActionEnabled)
})
}
func (h actionHandler) getActions(w http.ResponseWriter, r *http.Request) {
actions, err := h.service.List(r.Context())
if err != nil {
// encode error
h.encoder.Error(w, err)
return
}
h.encoder.StatusResponse(w, http.StatusOK, actions)
@ -57,13 +62,14 @@ func (h actionHandler) storeAction(w http.ResponseWriter, r *http.Request) {
)
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
// encode error
h.encoder.Error(w, err)
return
}
action, err := h.service.Store(ctx, data)
if err != nil {
// encode error
h.encoder.Error(w, err)
return
}
h.encoder.StatusResponse(w, http.StatusCreated, action)
@ -76,13 +82,14 @@ func (h actionHandler) updateAction(w http.ResponseWriter, r *http.Request) {
)
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
// encode error
h.encoder.Error(w, err)
return
}
action, err := h.service.Store(ctx, data)
if err != nil {
// encode error
h.encoder.Error(w, err)
return
}
h.encoder.StatusResponse(w, http.StatusCreated, action)
@ -91,11 +98,13 @@ func (h actionHandler) updateAction(w http.ResponseWriter, r *http.Request) {
func (h actionHandler) deleteAction(w http.ResponseWriter, r *http.Request) {
actionID, err := parseInt(chi.URLParam(r, "id"))
if err != nil {
h.encoder.StatusResponse(w, http.StatusBadRequest, errors.New("bad param id"))
h.encoder.StatusError(w, http.StatusBadRequest, errors.New("bad param id"))
return
}
if err := h.service.Delete(actionID); err != nil {
// encode error
if err := h.service.Delete(r.Context(), &domain.DeleteActionRequest{ActionId: actionID}); err != nil {
h.encoder.Error(w, err)
return
}
h.encoder.StatusResponse(w, http.StatusNoContent, nil)
@ -104,11 +113,13 @@ func (h actionHandler) deleteAction(w http.ResponseWriter, r *http.Request) {
func (h actionHandler) toggleActionEnabled(w http.ResponseWriter, r *http.Request) {
actionID, err := parseInt(chi.URLParam(r, "id"))
if err != nil {
h.encoder.StatusResponse(w, http.StatusBadRequest, errors.New("bad param id"))
h.encoder.StatusError(w, http.StatusBadRequest, errors.New("bad param id"))
return
}
if err := h.service.ToggleEnabled(actionID); err != nil {
// encode error
h.encoder.Error(w, err)
return
}
h.encoder.StatusResponse(w, http.StatusCreated, nil)

View file

@ -10,6 +10,7 @@ import (
"strconv"
"github.com/autobrr/autobrr/internal/domain"
"github.com/go-chi/chi/v5"
)
@ -19,6 +20,7 @@ type releaseService interface {
GetIndexerOptions(ctx context.Context) ([]string, error)
Stats(ctx context.Context) (*domain.ReleaseStats, error)
Delete(ctx context.Context) error
Retry(ctx context.Context, req *domain.ReleaseActionRetryReq) error
}
type releaseHandler struct {
@ -39,6 +41,10 @@ func (h releaseHandler) Routes(r chi.Router) {
r.Get("/stats", h.getStats)
r.Get("/indexers", h.getIndexerOptions)
r.Delete("/all", h.deleteReleases)
r.Route("/{releaseId}", func(r chi.Router) {
r.Post("/actions/{actionStatusId}/retry", h.retryAction)
})
}
func (h releaseHandler) findReleases(w http.ResponseWriter, r *http.Request) {
@ -186,3 +192,46 @@ func (h releaseHandler) deleteReleases(w http.ResponseWriter, r *http.Request) {
h.encoder.NoContent(w)
}
func (h releaseHandler) retryAction(w http.ResponseWriter, r *http.Request) {
var (
req *domain.ReleaseActionRetryReq
err error
)
releaseIdParam := chi.URLParam(r, "releaseId")
if releaseIdParam == "" {
h.encoder.StatusError(w, http.StatusBadRequest, err)
return
}
releaseId, err := strconv.Atoi(releaseIdParam)
if err != nil {
h.encoder.StatusError(w, http.StatusBadRequest, err)
return
}
actionStatusIdParam := chi.URLParam(r, "actionStatusId")
if actionStatusIdParam == "" {
h.encoder.StatusError(w, http.StatusBadRequest, err)
return
}
actionStatusId, err := strconv.Atoi(actionStatusIdParam)
if err != nil {
h.encoder.StatusError(w, http.StatusBadRequest, err)
return
}
req = &domain.ReleaseActionRetryReq{
ReleaseId: releaseId,
ActionStatusId: actionStatusId,
}
if err := h.service.Retry(r.Context(), req); err != nil {
h.encoder.Error(w, err)
return
}
h.encoder.NoContent(w)
}

View file

@ -19,6 +19,8 @@ import (
type Service interface {
Find(ctx context.Context, query domain.ReleaseQueryParams) (res []*domain.Release, nextCursor int64, count int64, err error)
FindRecent(ctx context.Context) ([]*domain.Release, error)
Get(ctx context.Context, req *domain.GetReleaseRequest) (*domain.Release, error)
GetActionStatus(ctx context.Context, req *domain.GetReleaseActionStatusRequest) (*domain.ReleaseActionStatus, error)
GetIndexerOptions(ctx context.Context) ([]string, error)
Stats(ctx context.Context) (*domain.ReleaseStats, error)
Store(ctx context.Context, release *domain.Release) error
@ -27,6 +29,7 @@ type Service interface {
Process(release *domain.Release)
ProcessMultiple(releases []*domain.Release)
Retry(ctx context.Context, req *domain.ReleaseActionRetryReq) error
}
type actionClientTypeKey struct {
@ -59,6 +62,14 @@ func (s *service) FindRecent(ctx context.Context) (res []*domain.Release, err er
return s.repo.FindRecent(ctx)
}
func (s *service) Get(ctx context.Context, req *domain.GetReleaseRequest) (*domain.Release, error) {
return s.repo.Get(ctx, req)
}
func (s *service) GetActionStatus(ctx context.Context, req *domain.GetReleaseActionStatusRequest) (*domain.ReleaseActionStatus, error) {
return s.repo.GetActionStatus(ctx, req)
}
func (s *service) GetIndexerOptions(ctx context.Context) ([]string, error) {
return s.repo.GetIndexerOptions(ctx)
}
@ -138,13 +149,13 @@ func (s *service) Process(release *domain.Release) {
}
if !match {
l.Trace().Msgf("release.Process: indexer: %s, filter: %s release: %s, no match. rejections: %s", release.Indexer, release.Filter.Name, release.TorrentName, release.RejectionsString())
l.Trace().Msgf("release.Process: indexer: %s, filter: %s release: %s, no match. rejections: %s", release.Indexer, release.FilterName, release.TorrentName, release.RejectionsString())
l.Debug().Msgf("release rejected: %s", release.RejectionsString())
continue
}
l.Info().Msgf("Matched '%s' (%s) for %s", release.TorrentName, release.Filter.Name, release.Indexer)
l.Info().Msgf("Matched '%s' (%s) for %s", release.TorrentName, release.FilterName, release.Indexer)
// save release here to only save those with rejections from actions instead of all releases
if release.ID == 0 {
@ -158,7 +169,7 @@ func (s *service) Process(release *domain.Release) {
// sleep for the delay period specified in the filter before running actions
delay := release.Filter.Delay
if delay > 0 {
l.Debug().Msgf("Delaying processing of '%s' (%s) for %s by %d seconds as specified in the filter", release.TorrentName, release.Filter.Name, release.Indexer, delay)
l.Debug().Msgf("Delaying processing of '%s' (%s) for %s by %d seconds as specified in the filter", release.TorrentName, release.FilterName, release.Indexer, delay)
time.Sleep(time.Duration(delay) * time.Second)
}
@ -170,30 +181,30 @@ func (s *service) Process(release *domain.Release) {
// only run enabled actions
if !act.Enabled {
l.Trace().Msgf("release.Process: indexer: %s, filter: %s release: %s action '%s' not enabled, skip", release.Indexer, release.Filter.Name, release.TorrentName, act.Name)
l.Trace().Msgf("release.Process: indexer: %s, filter: %s release: %s action '%s' not enabled, skip", release.Indexer, release.FilterName, release.TorrentName, act.Name)
continue
}
l.Trace().Msgf("release.Process: indexer: %s, filter: %s release: %s , run action: %s", release.Indexer, release.Filter.Name, release.TorrentName, act.Name)
l.Trace().Msgf("release.Process: indexer: %s, filter: %s release: %s , run action: %s", release.Indexer, release.FilterName, release.TorrentName, act.Name)
// keep track of actiom clients to avoid sending the same thing all over again
_, tried := triedActionClients[actionClientTypeKey{Type: act.Type, ClientID: act.ClientID}]
if tried {
l.Trace().Msgf("release.Process: indexer: %s, filter: %s release: %s action client already tried, skip", release.Indexer, release.Filter.Name, release.TorrentName)
l.Trace().Msgf("release.Process: indexer: %s, filter: %s release: %s action client already tried, skip", release.Indexer, release.FilterName, release.TorrentName)
continue
}
// run action
status, err := s.runAction(ctx, act, release)
if err != nil {
l.Error().Stack().Err(err).Msgf("release.Process: error running actions for filter: %s", release.Filter.Name)
l.Error().Stack().Err(err).Msgf("release.Process: error running actions for filter: %s", release.FilterName)
//continue
}
rejections = status.Rejections
if err := s.StoreReleaseActionStatus(ctx, status); err != nil {
s.log.Error().Err(err).Msgf("release.Process: error storing action status for filter: %s", release.Filter.Name)
s.log.Error().Err(err).Msgf("release.Process: error storing action status for filter: %s", release.FilterName)
}
if len(rejections) > 0 {
@ -237,12 +248,12 @@ func (s *service) runAction(ctx context.Context, action *domain.Action, release
status := domain.NewReleaseActionStatus(action, release)
if err := s.StoreReleaseActionStatus(ctx, status); err != nil {
s.log.Error().Err(err).Msgf("release.runAction: error storing action for filter: %s", release.Filter.Name)
s.log.Error().Err(err).Msgf("release.runAction: error storing action for filter: %s", release.FilterName)
}
rejections, err := s.actionSvc.RunAction(ctx, action, release)
if err != nil {
s.log.Error().Stack().Err(err).Msgf("release.runAction: error running actions for filter: %s", release.Filter.Name)
s.log.Error().Stack().Err(err).Msgf("release.runAction: error running actions for filter: %s", release.FilterName)
status.Status = domain.ReleasePushStatusErr
status.Rejections = []string{err.Error()}
@ -261,3 +272,54 @@ func (s *service) runAction(ctx context.Context, action *domain.Action, release
return status, nil
}
func (s *service) retryAction(ctx context.Context, action *domain.Action, release *domain.Release) error {
actionStatus, err := s.runAction(ctx, action, release)
if err != nil {
s.log.Error().Err(err).Msgf("release.retryAction: error running actions for filter: %s", release.FilterName)
if err := s.StoreReleaseActionStatus(ctx, actionStatus); err != nil {
s.log.Error().Err(err).Msgf("release.retryAction: error storing filterAction status for filter: %s", release.FilterName)
return err
}
return err
}
if err := s.StoreReleaseActionStatus(ctx, actionStatus); err != nil {
s.log.Error().Err(err).Msgf("release.retryAction: error storing filterAction status for filter: %s", release.FilterName)
return err
}
return nil
}
func (s *service) Retry(ctx context.Context, req *domain.ReleaseActionRetryReq) error {
// get release
release, err := s.Get(ctx, &domain.GetReleaseRequest{Id: req.ReleaseId})
if err != nil {
return err
}
// get release filter action status
status, err := s.GetActionStatus(ctx, &domain.GetReleaseActionStatusRequest{Id: req.ActionStatusId})
if err != nil {
return err
}
// get filter action with action id from status
filterAction, err := s.actionSvc.Get(ctx, &domain.GetActionRequest{Id: int(status.ActionID)})
if err != nil {
return err
}
// run filterAction
if err := s.retryAction(ctx, filterAction, release); err != nil {
s.log.Error().Err(err).Msgf("release.Retry: error re-running action: %s", filterAction.Name)
return err
}
s.log.Info().Msgf("successfully replayed action %s for release %s", filterAction.Name, release.TorrentName)
return nil
}

View file

@ -193,7 +193,8 @@ export const APIClient = {
},
indexerOptions: () => appClient.Get<string[]>("api/release/indexers"),
stats: () => appClient.Get<ReleaseStats>("api/release/stats"),
delete: () => appClient.Delete("api/release/all")
delete: () => appClient.Delete("api/release/all"),
replayAction: (releaseId: number, actionId: number) => appClient.Post(`api/release/${releaseId}/actions/${actionId}/retry`)
},
updates: {
check: () => appClient.Get("api/updates/check"),

View file

@ -5,11 +5,17 @@
import * as React from "react";
import { formatDistanceToNowStrict } from "date-fns";
import { CheckIcon } from "@heroicons/react/24/solid";
import { ArrowPathIcon, CheckIcon } from "@heroicons/react/24/solid";
import { ClockIcon, ExclamationCircleIcon, NoSymbolIcon } from "@heroicons/react/24/outline";
import { classNames, simplifyDate } from "@utils";
import { Tooltip } from "../tooltips/Tooltip";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { APIClient } from "@api/APIClient";
import { filterKeys } from "@screens/filters/list";
import { toast } from "react-hot-toast";
import Toast from "@components/notifications/Toast";
import { RingResizeSpinner } from "@components/Icons";
interface CellProps {
value: string;
@ -57,6 +63,46 @@ export const TitleCell = ({ value }: CellProps) => (
</div>
);
interface RetryActionButtonProps {
status: ReleaseActionStatus;
}
interface RetryAction {
releaseId: number;
actionId: number;
}
const RetryActionButton = ({ status }: RetryActionButtonProps) => {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (vars: RetryAction) => APIClient.release.replayAction(vars.releaseId, vars.actionId),
onSuccess: () => {
// Invalidate filters just in case, most likely not necessary but can't hurt.
queryClient.invalidateQueries({ queryKey: filterKeys.lists() });
toast.custom((t) => (
<Toast type="success" body={`${status?.action} replayed`} t={t} />
));
}
});
const replayAction = () => {
console.log("replay action");
mutation.mutate({ releaseId: status.release_id,actionId: status.id });
};
return (
<button className="flex items-center px-1.5 py-1 ml-2 border-gray-500 bg-gray-700 rounded hover:bg-gray-600" onClick={replayAction}>
<span className="mr-1.5">Retry</span>
{mutation.isLoading
? <RingResizeSpinner className="text-blue-500 w-4 h-4 iconHeight" aria-hidden="true" />
: <ArrowPathIcon className="h-4 w-4" />
}
</button>
);
};
interface ReleaseStatusCellProps {
value: ReleaseActionStatus[];
}
@ -64,69 +110,89 @@ interface ReleaseStatusCellProps {
interface StatusCellMapEntry {
colors: string;
icon: React.ReactElement;
textFormatter: (text: string) => React.ReactElement;
textFormatter: (status: ReleaseActionStatus) => React.ReactElement;
}
const StatusCellMap: Record<string, StatusCellMapEntry> = {
"PUSH_ERROR": {
colors: "bg-pink-100 text-pink-800 hover:bg-pink-300",
icon: <ExclamationCircleIcon className="h-5 w-5" aria-hidden="true" />,
textFormatter: (text: string) => (
textFormatter: (status: ReleaseActionStatus) => (
<>
<span>
Action
{" "}
<span className="font-bold underline underline-offset-2 decoration-2 decoration-red-500">
{" "}
<span className="font-bold underline underline-offset-2 decoration-2 decoration-red-500">
error
</span>
{": "}
{status.action}
</span>
{": "}
{text}
<div>
{status.action_id > 0 && <RetryActionButton status={status} />}
</div>
</>
)
},
"PUSH_REJECTED": {
colors: "bg-blue-100 dark:bg-blue-100 text-blue-400 dark:text-blue-800 hover:bg-blue-300 dark:hover:bg-blue-400",
icon: <NoSymbolIcon className="h-5 w-5" aria-hidden="true" />,
textFormatter: (text: string) => (
textFormatter: (status: ReleaseActionStatus) => (
<>
<span>
Action
{" "}
<span
className="font-bold underline underline-offset-2 decoration-2 decoration-sky-500"
>
{" "}
<span
className="font-bold underline underline-offset-2 decoration-2 decoration-sky-500"
>
rejected
</span>
{": "}
{status.action}
</span>
{": "}
{text}
<div>
{status.action_id > 0 && <RetryActionButton status={status} />}
</div>
</>
)
},
"PUSH_APPROVED": {
colors: "bg-green-100 text-green-800 hover:bg-green-300",
icon: <CheckIcon className="h-5 w-5" aria-hidden="true" />,
textFormatter: (text: string) => (
textFormatter: (status: ReleaseActionStatus) => (
<>
Action
{" "}
<span className="font-bold underline underline-offset-2 decoration-2 decoration-green-500">
<span>
Action
{" "}
<span className="font-bold underline underline-offset-2 decoration-2 decoration-green-500">
approved
</span>
{": "}
{status.action}
</span>
{": "}
{text}
{/*<div>*/}
{/* {status.action_id > 0 && <RetryActionButton status={status} />}*/}
{/*</div>*/}
</>
)
},
"PENDING": {
colors: "bg-yellow-100 text-yellow-800 hover:bg-yellow-200",
icon: <ClockIcon className="h-5 w-5" aria-hidden="true" />,
textFormatter: (text: string) => (
textFormatter: (status: ReleaseActionStatus) => (
<>
Action
{" "}
<span className="font-bold underline underline-offset-2 decoration-2 decoration-yellow-500">
<span>
Action
{" "}
<span className="font-bold underline underline-offset-2 decoration-2 decoration-yellow-500">
pending
</span>
{": "}
{status.action}
</span>
{": "}
{text}
<div>
{status.action_id > 0 && <RetryActionButton status={status} />}
</div>
</>
)
}
@ -156,7 +222,7 @@ export const ReleaseStatusCell = ({ value }: ReleaseStatusCellProps) => (
>
<Tooltip
label={StatusCellMap[v.status].icon}
title={StatusCellMap[v.status].textFormatter(v.action)}
title={StatusCellMap[v.status].textFormatter(v)}
>
<div className="mb-1">
<CellLine title="Type">{v.type}</CellLine>

View file

@ -48,7 +48,7 @@ export const Tooltip = ({
})}
>
{title ? (
<div className="p-2 border-b border-gray-300 bg-gray-100 dark:border-gray-700 dark:bg-gray-800 rounded-t-md">
<div className="flex justify-between items-center p-2 border-b border-gray-300 bg-gray-100 dark:border-gray-700 dark:bg-gray-800 rounded-t-md">
{title}
</div>
) : null}

View file

@ -4,11 +4,12 @@
*/
import React, { Fragment, useEffect, useRef, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { useMutation, useQuery } from "@tanstack/react-query";
import { Field, FieldArray, FieldProps, FormikValues, useFormikContext } from "formik";
import { Dialog, Switch as SwitchBasic, Transition } from "@headlessui/react";
import { ChevronRightIcon } from "@heroicons/react/24/solid";
import { Link } from "react-router-dom";
import { toast } from "react-hot-toast";
import {
ActionContentLayoutOptions,
@ -25,6 +26,7 @@ import { classNames } from "@utils";
import { DeleteModal } from "@components/modals";
import { CollapsableSection } from "./details";
import { TextArea } from "@components/inputs/input";
import Toast from "@components/notifications/Toast";
interface FilterActionsProps {
filter: Filter;
@ -543,6 +545,23 @@ function FilterActionsItem({ action, clients, idx, initialEdit, remove }: Filter
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
const [edit, toggleEdit] = useToggle(initialEdit);
const removeMutation = useMutation({
mutationFn: (id: number) => APIClient.actions.delete(id),
onSuccess: () => {
remove(idx);
// Invalidate filters just in case, most likely not necessary but can't hurt.
// queryClient.invalidateQueries({ queryKey: filterKeys.detail(id) });
toast.custom((t) => (
<Toast type="success" body={`Action ${action?.name} was deleted`} t={t} />
));
}
});
const removeAction = (id: number) => {
removeMutation.mutate(id);
};
return (
<li>
<div
@ -622,7 +641,7 @@ function FilterActionsItem({ action, clients, idx, initialEdit, remove }: Filter
isOpen={deleteModalIsOpen}
buttonRef={cancelButtonRef}
toggle={toggleDeleteModal}
deleteAction={() => remove(idx)}
deleteAction={() => removeAction(action.id)}
title="Remove filter action"
text="Are you sure you want to remove this action? This action cannot be undone."
/>

View file

@ -23,9 +23,12 @@ interface ReleaseActionStatus {
id: number;
status: string;
action: string;
action_id: number;
type: string;
client: string;
filter: string;
filter_id: number;
release_id: number;
rejections: string[];
timestamp: string
}