feat(filters): duplicate (#168)

This commit is contained in:
Ludvig Lundgren 2022-03-06 16:08:07 +01:00 committed by GitHub
parent 72b74f9d19
commit cb6cbb83d5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 115 additions and 12 deletions

View file

@ -217,7 +217,7 @@ func (r *FilterRepo) FindByIndexerIdentifier(indexer string) ([]domain.Filter, e
return filters, nil return filters, nil
} }
func (r *FilterRepo) Store(filter domain.Filter) (*domain.Filter, error) { func (r *FilterRepo) Store(ctx context.Context, filter domain.Filter) (*domain.Filter, error) {
//r.db.lock.RLock() //r.db.lock.RLock()
//defer r.db.lock.RUnlock() //defer r.db.lock.RUnlock()
@ -227,7 +227,7 @@ func (r *FilterRepo) Store(filter domain.Filter) (*domain.Filter, error) {
} else { } else {
var res sql.Result var res sql.Result
res, err = r.db.handler.Exec(`INSERT INTO filter ( res, err = r.db.handler.ExecContext(ctx, `INSERT INTO filter (
name, name,
enabled, enabled,
min_size, min_size,

View file

@ -14,7 +14,7 @@ type FilterRepo interface {
FindByID(ctx context.Context, filterID int) (*Filter, error) FindByID(ctx context.Context, filterID int) (*Filter, error)
FindByIndexerIdentifier(indexer string) ([]Filter, error) FindByIndexerIdentifier(indexer string) ([]Filter, error)
ListFilters(ctx context.Context) ([]Filter, error) ListFilters(ctx context.Context) ([]Filter, error)
Store(filter Filter) (*Filter, error) Store(ctx context.Context, filter Filter) (*Filter, error)
Update(ctx context.Context, filter Filter) (*Filter, error) Update(ctx context.Context, filter Filter) (*Filter, error)
ToggleEnabled(ctx context.Context, filterID int, enabled bool) error ToggleEnabled(ctx context.Context, filterID int, enabled bool) error
Delete(ctx context.Context, filterID int) error Delete(ctx context.Context, filterID int) error

View file

@ -3,6 +3,7 @@ package filter
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@ -16,8 +17,9 @@ type Service interface {
FindByIndexerIdentifier(indexer string) ([]domain.Filter, error) FindByIndexerIdentifier(indexer string) ([]domain.Filter, error)
FindAndCheckFilters(release *domain.Release) (bool, *domain.Filter, error) FindAndCheckFilters(release *domain.Release) (bool, *domain.Filter, error)
ListFilters(ctx context.Context) ([]domain.Filter, error) ListFilters(ctx context.Context) ([]domain.Filter, error)
Store(filter domain.Filter) (*domain.Filter, error) Store(ctx context.Context, filter domain.Filter) (*domain.Filter, error)
Update(ctx context.Context, filter domain.Filter) (*domain.Filter, error) Update(ctx context.Context, filter domain.Filter) (*domain.Filter, error)
Duplicate(ctx context.Context, filterID int) (*domain.Filter, error)
ToggleEnabled(ctx context.Context, filterID int, enabled bool) error ToggleEnabled(ctx context.Context, filterID int, enabled bool) error
Delete(ctx context.Context, filterID int) error Delete(ctx context.Context, filterID int) error
} }
@ -96,11 +98,11 @@ func (s *service) FindByIndexerIdentifier(indexer string) ([]domain.Filter, erro
return filters, nil return filters, nil
} }
func (s *service) Store(filter domain.Filter) (*domain.Filter, error) { func (s *service) Store(ctx context.Context, filter domain.Filter) (*domain.Filter, error) {
// validate data // validate data
// store // store
f, err := s.repo.Store(filter) f, err := s.repo.Store(ctx, filter)
if err != nil { if err != nil {
log.Error().Err(err).Msgf("could not store filter: %v", filter) log.Error().Err(err).Msgf("could not store filter: %v", filter)
return nil, err return nil, err
@ -140,6 +142,56 @@ func (s *service) Update(ctx context.Context, filter domain.Filter) (*domain.Fil
return f, nil return f, nil
} }
func (s *service) Duplicate(ctx context.Context, filterID int) (*domain.Filter, error) {
// find filter
baseFilter, err := s.repo.FindByID(ctx, filterID)
if err != nil {
return nil, err
}
baseFilter.ID = 0
baseFilter.Name = fmt.Sprintf("%v Copy", baseFilter.Name)
baseFilter.Enabled = false
// find actions and attach
filterActions, err := s.actionRepo.FindByFilterID(ctx, filterID)
if err != nil {
log.Error().Msgf("could not find filter actions: %+v", &filterID)
return nil, err
}
// find indexers and attach
filterIndexers, err := s.indexerSvc.FindByFilterID(ctx, filterID)
if err != nil {
log.Error().Err(err).Msgf("could not find indexers for filter: %+v", &baseFilter.Name)
return nil, err
}
// update
filter, err := s.repo.Store(ctx, *baseFilter)
if err != nil {
log.Error().Err(err).Msgf("could not update filter: %v", baseFilter.Name)
return nil, err
}
// take care of connected indexers
if err = s.repo.StoreIndexerConnections(ctx, filter.ID, filterIndexers); err != nil {
log.Error().Err(err).Msgf("could not store filter indexer connections: %v", filter.Name)
return nil, err
}
filter.Indexers = filterIndexers
// take care of filter actions
actions, err := s.actionRepo.StoreFilterActions(ctx, filterActions, int64(filter.ID))
if err != nil {
log.Error().Err(err).Msgf("could not store filter actions: %v", filter.Name)
return nil, err
}
filter.Actions = actions
return filter, nil
}
func (s *service) ToggleEnabled(ctx context.Context, filterID int, enabled bool) error { func (s *service) ToggleEnabled(ctx context.Context, filterID int, enabled bool) error {
if err := s.repo.ToggleEnabled(ctx, filterID, enabled); err != nil { if err := s.repo.ToggleEnabled(ctx, filterID, enabled); err != nil {
log.Error().Err(err).Msg("could not update filter enabled") log.Error().Err(err).Msg("could not update filter enabled")

View file

@ -14,9 +14,10 @@ import (
type filterService interface { type filterService interface {
ListFilters(ctx context.Context) ([]domain.Filter, error) ListFilters(ctx context.Context) ([]domain.Filter, error)
FindByID(ctx context.Context, filterID int) (*domain.Filter, error) FindByID(ctx context.Context, filterID int) (*domain.Filter, error)
Store(filter domain.Filter) (*domain.Filter, error) Store(ctx context.Context, filter domain.Filter) (*domain.Filter, error)
Delete(ctx context.Context, filterID int) error Delete(ctx context.Context, filterID int) error
Update(ctx context.Context, filter domain.Filter) (*domain.Filter, error) Update(ctx context.Context, filter domain.Filter) (*domain.Filter, error)
Duplicate(ctx context.Context, filterID int) (*domain.Filter, error)
ToggleEnabled(ctx context.Context, filterID int, enabled bool) error ToggleEnabled(ctx context.Context, filterID int, enabled bool) error
} }
@ -35,6 +36,7 @@ func newFilterHandler(encoder encoder, service filterService) *filterHandler {
func (h filterHandler) Routes(r chi.Router) { func (h filterHandler) Routes(r chi.Router) {
r.Get("/", h.getFilters) r.Get("/", h.getFilters)
r.Get("/{filterID}", h.getByID) r.Get("/{filterID}", h.getByID)
r.Get("/{filterID}/duplicate", h.duplicate)
r.Post("/", h.store) r.Post("/", h.store)
r.Put("/{filterID}", h.update) r.Put("/{filterID}", h.update)
r.Put("/{filterID}/enabled", h.toggleEnabled) r.Put("/{filterID}/enabled", h.toggleEnabled)
@ -69,6 +71,23 @@ func (h filterHandler) getByID(w http.ResponseWriter, r *http.Request) {
h.encoder.StatusResponse(ctx, w, filter, http.StatusOK) h.encoder.StatusResponse(ctx, w, filter, http.StatusOK)
} }
func (h filterHandler) duplicate(w http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
filterID = chi.URLParam(r, "filterID")
)
id, _ := strconv.Atoi(filterID)
filter, err := h.service.Duplicate(ctx, id)
if err != nil {
h.encoder.StatusNotFound(ctx, w)
return
}
h.encoder.StatusResponse(ctx, w, filter, http.StatusOK)
}
func (h filterHandler) store(w http.ResponseWriter, r *http.Request) { func (h filterHandler) store(w http.ResponseWriter, r *http.Request) {
var ( var (
ctx = r.Context() ctx = r.Context()
@ -80,7 +99,7 @@ func (h filterHandler) store(w http.ResponseWriter, r *http.Request) {
return return
} }
filter, err := h.service.Store(data) filter, err := h.service.Store(ctx, data)
if err != nil { if err != nil {
// encode error // encode error
return return

View file

@ -72,6 +72,7 @@ export const APIClient = {
getByID: (id: number) => appClient.Get<Filter>(`api/filters/${id}`), getByID: (id: number) => appClient.Get<Filter>(`api/filters/${id}`),
create: (filter: Filter) => appClient.Post("api/filters", filter), create: (filter: Filter) => appClient.Post("api/filters", filter),
update: (filter: Filter) => appClient.Put(`api/filters/${filter.id}`, filter), update: (filter: Filter) => appClient.Put(`api/filters/${filter.id}`, filter),
duplicate: (id: number) => appClient.Get<Filter>(`api/filters/${id}/duplicate`),
toggleEnable: (id: number, enabled: boolean) => appClient.Put(`api/filters/${id}/enabled`, { enabled }), toggleEnable: (id: number, enabled: boolean) => appClient.Put(`api/filters/${id}/enabled`, { enabled }),
delete: (id: number) => appClient.Delete(`api/filters/${id}`), delete: (id: number) => appClient.Delete(`api/filters/${id}`),
}, },

View file

@ -7,7 +7,7 @@ import {
TrashIcon, TrashIcon,
PencilAltIcon, PencilAltIcon,
SwitchHorizontalIcon, SwitchHorizontalIcon,
DotsHorizontalIcon, DotsHorizontalIcon, DuplicateIcon,
} from "@heroicons/react/outline"; } from "@heroicons/react/outline";
import { queryClient } from "../../App"; import { queryClient } from "../../App";
@ -125,6 +125,17 @@ const FilterItemDropdown = ({
} }
); );
const duplicateMutation = useMutation(
(id: number) => APIClient.filters.duplicate(id),
{
onSuccess: () => {
queryClient.invalidateQueries("filters");
toast.custom((t) => <Toast type="success" body={`Filter ${filter?.name} duplicated`} t={t} />);
}
}
);
return ( return (
<Menu as="div"> <Menu as="div">
<DeleteModal <DeleteModal
@ -197,8 +208,6 @@ const FilterItemDropdown = ({
</button> </button>
)} )}
</Menu.Item> </Menu.Item>
</div>
<div className="px-1 py-1">
<Menu.Item> <Menu.Item>
{({ active }) => ( {({ active }) => (
<button <button
@ -206,11 +215,33 @@ const FilterItemDropdown = ({
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300", active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm" "font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
)} )}
onClick={() => duplicateMutation.mutate(filter.id)}
>
<DuplicateIcon
className={classNames(
active ? "text-white" : "text-blue-500",
"w-5 h-5 mr-2"
)}
aria-hidden="true"
/>
Duplicate
</button>
)}
</Menu.Item>
</div>
<div className="px-1 py-1">
<Menu.Item>
{({ active }) => (
<button
className={classNames(
active ? "bg-red-600 text-white" : "text-gray-900 dark:text-gray-300",
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
)}
onClick={() => toggleDeleteModal()} onClick={() => toggleDeleteModal()}
> >
<TrashIcon <TrashIcon
className={classNames( className={classNames(
active ? "text-white" : "text-blue-500", active ? "text-white" : "text-red-500",
"w-5 h-5 mr-2" "w-5 h-5 mr-2"
)} )}
aria-hidden="true" aria-hidden="true"