feat(releases): action status show filter and client (#338)

* feat(releases): action status show client and filter

* feat(releases): add better tooltip
This commit is contained in:
Ludvig Lundgren 2022-07-06 17:30:41 +02:00 committed by GitHub
parent a1ce74761e
commit 31fbe013ff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 106 additions and 46 deletions

View file

@ -137,6 +137,8 @@ func (s *service) RunAction(action *domain.Action, release domain.Release) ([]st
Status: domain.ReleasePushStatusApproved, Status: domain.ReleasePushStatusApproved,
Action: action.Name, Action: action.Name,
Type: action.Type, Type: action.Type,
Client: action.Client.Name,
Filter: release.Filter.Name,
Rejections: []string{}, Rejections: []string{},
Timestamp: time.Now(), Timestamp: time.Now(),
} }

View file

@ -225,6 +225,8 @@ CREATE TABLE release_action_status
status TEXT, status TEXT,
action TEXT NOT NULL, action TEXT NOT NULL,
type TEXT NOT NULL, type TEXT NOT NULL,
client TEXT,
filter TEXT,
rejections TEXT [] DEFAULT '{}' NOT NULL, rejections TEXT [] DEFAULT '{}' NOT NULL,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
raw TEXT, raw TEXT,
@ -805,6 +807,13 @@ CREATE INDEX release_torrent_name_index
CREATE INDEX indexer_identifier_index CREATE INDEX indexer_identifier_index
ON indexer (identifier); ON indexer (identifier);
`, `,
`
ALTER TABLE release_action_status
ADD COLUMN client;
ALTER TABLE release_action_status
ADD COLUMN filter;
`,
} }
const postgresSchema = ` const postgresSchema = `
@ -1049,6 +1058,8 @@ CREATE TABLE release_action_status
status TEXT, status TEXT,
action TEXT NOT NULL, action TEXT NOT NULL,
type TEXT NOT NULL, type TEXT NOT NULL,
client TEXT,
filter TEXT,
rejections TEXT [] DEFAULT '{}' NOT NULL, rejections TEXT [] DEFAULT '{}' NOT NULL,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
raw TEXT, raw TEXT,
@ -1292,4 +1303,11 @@ CREATE INDEX release_torrent_name_index
CREATE INDEX indexer_identifier_index CREATE INDEX indexer_identifier_index
ON indexer (identifier); ON indexer (identifier);
`, `,
`
ALTER TABLE release_action_status
ADD COLUMN client;
ALTER TABLE release_action_status
ADD COLUMN filter;
`,
} }

View file

@ -75,8 +75,8 @@ func (repo *ReleaseRepo) StoreReleaseActionStatus(ctx context.Context, a *domain
} else { } else {
queryBuilder := repo.db.squirrel. queryBuilder := repo.db.squirrel.
Insert("release_action_status"). Insert("release_action_status").
Columns("status", "action", "type", "rejections", "timestamp", "release_id"). Columns("status", "action", "type", "client", "filter", "rejections", "timestamp", "release_id").
Values(a.Status, a.Action, a.Type, pq.Array(a.Rejections), a.Timestamp, a.ReleaseID). Values(a.Status, a.Action, a.Type, a.Client, a.Filter, pq.Array(a.Rejections), a.Timestamp, a.ReleaseID).
Suffix("RETURNING id").RunWith(repo.db.handler) Suffix("RETURNING id").RunWith(repo.db.handler)
// return values // return values
@ -160,11 +160,12 @@ func (repo *ReleaseRepo) findReleases(ctx context.Context, tx *Tx, params domain
} }
query, args, err := queryBuilder.ToSql() query, args, err := queryBuilder.ToSql()
repo.log.Trace().Str("database", "release.find").Msgf("query: '%v', args: '%v'", query, args)
if err != nil { if err != nil {
return nil, 0, 0, errors.Wrap(err, "error building query") return nil, 0, 0, errors.Wrap(err, "error building query")
} }
repo.log.Trace().Str("database", "release.find").Msgf("query: '%v', args: '%v'", query, args)
res := make([]*domain.Release, 0) res := make([]*domain.Release, 0)
rows, err := tx.QueryContext(ctx, query, args...) rows, err := tx.QueryContext(ctx, query, args...)
@ -207,7 +208,7 @@ func (repo *ReleaseRepo) findReleases(ctx context.Context, tx *Tx, params domain
func (repo *ReleaseRepo) FindRecent(ctx context.Context) ([]*domain.Release, error) { func (repo *ReleaseRepo) FindRecent(ctx context.Context) ([]*domain.Release, error) {
tx, err := repo.db.BeginTx(ctx, &sql.TxOptions{}) tx, err := repo.db.BeginTx(ctx, &sql.TxOptions{})
if err != nil { if err != nil {
return nil, err return nil, errors.Wrap(err, "error begin transaction")
} }
defer tx.Rollback() defer tx.Rollback()
@ -225,8 +226,7 @@ func (repo *ReleaseRepo) FindRecent(ctx context.Context) ([]*domain.Release, err
} }
if err = tx.Commit(); err != nil { if err = tx.Commit(); err != nil {
repo.log.Error().Stack().Err(err).Msg("error finding releases") return nil, errors.Wrap(err, "error transaction commit")
return nil, err
} }
return releases, nil return releases, nil
@ -240,25 +240,23 @@ func (repo *ReleaseRepo) findRecentReleases(ctx context.Context, tx *Tx) ([]*dom
Limit(10) Limit(10)
query, args, err := queryBuilder.ToSql() query, args, err := queryBuilder.ToSql()
repo.log.Trace().Str("database", "release.find").Msgf("query: '%v', args: '%v'", query, args)
if err != nil { if err != nil {
repo.log.Error().Stack().Err(err).Msg("error building query") return nil, errors.Wrap(err, "error building query")
return nil, err
} }
repo.log.Trace().Str("database", "release.find").Msgf("query: '%v', args: '%v'", query, args)
res := make([]*domain.Release, 0) res := make([]*domain.Release, 0)
rows, err := tx.QueryContext(ctx, query, args...) rows, err := tx.QueryContext(ctx, query, args...)
if err != nil { if err != nil {
repo.log.Error().Stack().Err(err).Msg("error fetching releases") return res, errors.Wrap(err, "error executing query")
return res, nil
} }
defer rows.Close() defer rows.Close()
if err := rows.Err(); err != nil { if err := rows.Err(); err != nil {
repo.log.Error().Stack().Err(err) return res, errors.Wrap(err, "rows error")
return res, err
} }
for rows.Next() { for rows.Next() {
@ -267,8 +265,7 @@ func (repo *ReleaseRepo) findRecentReleases(ctx context.Context, tx *Tx) ([]*dom
var indexer, filter sql.NullString var indexer, filter sql.NullString
if err := rows.Scan(&rls.ID, &rls.FilterStatus, pq.Array(&rls.Rejections), &indexer, &filter, &rls.Protocol, &rls.Title, &rls.TorrentName, &rls.Size, &rls.Timestamp); err != nil { if err := rows.Scan(&rls.ID, &rls.FilterStatus, pq.Array(&rls.Rejections), &indexer, &filter, &rls.Protocol, &rls.Title, &rls.TorrentName, &rls.Size, &rls.Timestamp); err != nil {
repo.log.Error().Stack().Err(err).Msg("release.find: error scanning data to struct") return res, errors.Wrap(err, "error scanning row")
return res, err
} }
rls.Indexer = indexer.String rls.Indexer = indexer.String
@ -290,23 +287,20 @@ func (repo *ReleaseRepo) GetIndexerOptions(ctx context.Context) ([]string, error
rows, err := repo.db.handler.QueryContext(ctx, query) rows, err := repo.db.handler.QueryContext(ctx, query)
if err != nil { if err != nil {
repo.log.Error().Stack().Err(err).Msg("error fetching indexer list") return res, errors.Wrap(err, "error executing query")
return res, err
} }
defer rows.Close() defer rows.Close()
if err := rows.Err(); err != nil { if err := rows.Err(); err != nil {
repo.log.Error().Stack().Err(err) return res, errors.Wrap(err, "rows error")
return res, err
} }
for rows.Next() { for rows.Next() {
var indexer string var indexer string
if err := rows.Scan(&indexer); err != nil { if err := rows.Scan(&indexer); err != nil {
repo.log.Error().Stack().Err(err).Msg("release.find: error scanning data to struct") return res, errors.Wrap(err, "error scanning row")
return res, err
} }
res = append(res, indexer) res = append(res, indexer)
@ -318,18 +312,20 @@ func (repo *ReleaseRepo) GetIndexerOptions(ctx context.Context) ([]string, error
func (repo *ReleaseRepo) GetActionStatusByReleaseID(ctx context.Context, releaseID int64) ([]domain.ReleaseActionStatus, error) { func (repo *ReleaseRepo) GetActionStatusByReleaseID(ctx context.Context, releaseID int64) ([]domain.ReleaseActionStatus, error) {
queryBuilder := repo.db.squirrel. queryBuilder := repo.db.squirrel.
Select("id", "status", "action", "type", "rejections", "timestamp"). Select("id", "status", "action", "type", "client", "filter", "rejections", "timestamp").
From("release_action_status"). From("release_action_status").
Where("release_id = ?", releaseID) Where("release_id = ?", releaseID)
query, args, err := queryBuilder.ToSql() query, args, err := queryBuilder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "error building query")
}
res := make([]domain.ReleaseActionStatus, 0) res := make([]domain.ReleaseActionStatus, 0)
rows, err := repo.db.handler.QueryContext(ctx, query, args...) rows, err := repo.db.handler.QueryContext(ctx, query, args...)
if err != nil { if err != nil {
repo.log.Error().Stack().Err(err).Msg("error fetching releases") return res, errors.Wrap(err, "error executing query")
return res, nil
} }
defer rows.Close() defer rows.Close()
@ -342,11 +338,15 @@ func (repo *ReleaseRepo) GetActionStatusByReleaseID(ctx context.Context, release
for rows.Next() { for rows.Next() {
var rls domain.ReleaseActionStatus var rls domain.ReleaseActionStatus
if err := rows.Scan(&rls.ID, &rls.Status, &rls.Action, &rls.Type, pq.Array(&rls.Rejections), &rls.Timestamp); err != nil { var client, filter sql.NullString
repo.log.Error().Stack().Err(err).Msg("release.find: error scanning data to struct")
return res, err if err := rows.Scan(&rls.ID, &rls.Status, &rls.Action, &rls.Type, &client, &filter, pq.Array(&rls.Rejections), &rls.Timestamp); err != nil {
return res, errors.Wrap(err, "error scanning row")
} }
rls.Client = client.String
rls.Filter = filter.String
res = append(res, rls) res = append(res, rls)
} }
@ -356,11 +356,14 @@ func (repo *ReleaseRepo) GetActionStatusByReleaseID(ctx context.Context, release
func (repo *ReleaseRepo) attachActionStatus(ctx context.Context, tx *Tx, releaseID int64) ([]domain.ReleaseActionStatus, error) { func (repo *ReleaseRepo) attachActionStatus(ctx context.Context, tx *Tx, releaseID int64) ([]domain.ReleaseActionStatus, error) {
queryBuilder := repo.db.squirrel. queryBuilder := repo.db.squirrel.
Select("id", "status", "action", "type", "rejections", "timestamp"). Select("id", "status", "action", "type", "client", "filter", "rejections", "timestamp").
From("release_action_status"). From("release_action_status").
Where("release_id = ?", releaseID) Where("release_id = ?", releaseID)
query, args, err := queryBuilder.ToSql() query, args, err := queryBuilder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "error building query")
}
res := make([]domain.ReleaseActionStatus, 0) res := make([]domain.ReleaseActionStatus, 0)
@ -378,10 +381,15 @@ func (repo *ReleaseRepo) attachActionStatus(ctx context.Context, tx *Tx, release
for rows.Next() { for rows.Next() {
var rls domain.ReleaseActionStatus var rls domain.ReleaseActionStatus
if err := rows.Scan(&rls.ID, &rls.Status, &rls.Action, &rls.Type, pq.Array(&rls.Rejections), &rls.Timestamp); err != nil { var client, filter sql.NullString
if err := rows.Scan(&rls.ID, &rls.Status, &rls.Action, &rls.Type, &client, &filter, pq.Array(&rls.Rejections), &rls.Timestamp); err != nil {
return res, errors.Wrap(err, "error scanning row") return res, errors.Wrap(err, "error scanning row")
} }
rls.Client = client.String
rls.Filter = filter.String
res = append(res, rls) res = append(res, rls)
} }

View file

@ -92,6 +92,8 @@ type ReleaseActionStatus struct {
Status ReleasePushStatus `json:"status"` Status ReleasePushStatus `json:"status"`
Action string `json:"action"` Action string `json:"action"`
Type ActionType `json:"type"` Type ActionType `json:"type"`
Client string `json:"client"`
Filter string `json:"filter"`
Rejections []string `json:"rejections"` Rejections []string `json:"rejections"`
Timestamp time.Time `json:"timestamp"` Timestamp time.Time `json:"timestamp"`
ReleaseID int64 `json:"-"` ReleaseID int64 `json:"-"`

View file

@ -4,6 +4,7 @@ import { CheckIcon } from "@heroicons/react/solid";
import { ClockIcon, BanIcon, ExclamationCircleIcon } from "@heroicons/react/outline"; import { ClockIcon, BanIcon, ExclamationCircleIcon } from "@heroicons/react/outline";
import { classNames, simplifyDate } from "../../utils"; import { classNames, simplifyDate } from "../../utils";
import { Tooltip } from "../tooltips/Tooltip";
interface CellProps { interface CellProps {
value: string; value: string;
@ -52,30 +53,42 @@ const StatusCellMap: Record<string, StatusCellMapEntry> = {
} }
}; };
const GetReleaseStatusString = (releaseAction: ReleaseActionStatus) => { // const GetReleaseStatusString = (releaseAction: ReleaseActionStatus) => {
const items: Array<string> = [ // const items: Array<string> = [
`action: ${releaseAction.action}`, // `action: ${releaseAction.action}`,
`type: ${releaseAction.type}`, // `type: ${releaseAction.type}`,
`status: ${releaseAction.status}`, // `status: ${releaseAction.status}`,
`time: ${simplifyDate(releaseAction.timestamp)}` // `time: ${simplifyDate(releaseAction.timestamp)}`
]; // ];
if (releaseAction.rejections.length) // if (releaseAction.client != "")
items.push(`rejections: ${releaseAction.rejections}`); // items.push(`client: ${releaseAction.client}`);
return items.join(" | "); // if (releaseAction.filter != "")
}; // items.push(`filter: ${releaseAction.filter}`);
// if (releaseAction.rejections.length)
// items.push(`rejections: ${releaseAction.rejections}`);
// return items.join(" | ");
// };
export const ReleaseStatusCell = ({ value }: ReleaseStatusCellProps) => ( export const ReleaseStatusCell = ({ value }: ReleaseStatusCellProps) => (
<div className="flex text-sm font-medium text-gray-900 dark:text-gray-300"> <div className="flex text-sm font-medium text-gray-900 dark:text-gray-300">
{value.map((v, idx) => ( {value.map((v, idx) => (
<div <div
key={idx} key={idx}
title={GetReleaseStatusString(v)}
className={classNames( className={classNames(
StatusCellMap[v.status].colors, StatusCellMap[v.status].colors,
"mr-1 inline-flex items-center rounded text-xs font-semibold uppercase cursor-pointer" "mr-1 inline-flex items-center rounded text-xs font-semibold cursor-pointer"
)} )}
> >
{StatusCellMap[v.status].icon} <Tooltip button={StatusCellMap[v.status].icon}>
<ol className="flex flex-col">
<li className="py-1">Status: {v.status}</li>
<li className="py-1">Action: {v.action}</li>
<li className="py-1">Type: {v.type}</li>
{v.client && <li className="py-1">Client: {v.client}</li>}
{v.filter && <li className="py-1">Filter: {v.filter}</li>}
<li className="py-1">Time: {simplifyDate(v.timestamp)}</li>
</ol>
</Tooltip>
</div> </div>
))} ))}
</div> </div>

View file

@ -0,0 +1,15 @@
import { ReactNode } from "react";
export const Tooltip = ({ children, button } : {
message?: string, children: ReactNode, button: ReactNode
}) => {
return (
<div className="relative flex flex-col items-center group">
{button}
<div className="absolute bottom-0 flex flex-col items-center hidden mb-6 group-hover:flex">
<span className="relative z-40 p-2 text-xs leading-none text-white whitespace-no-wrap bg-gray-600 shadow-lg rounded-md">{children}</span>
<div className="w-3 h-3 -mt-2 rotate-45 bg-gray-600"></div>
</div>
</div>
);
};

View file

@ -191,7 +191,7 @@ export const ReleaseTable = () => {
)) ))
)} )}
</div> </div>
<div className="overflow-auto bg-white shadow-lg dark:bg-gray-800 rounded-lg"> <div className="bg-white shadow-lg dark:bg-gray-800 rounded-lg">
<table {...getTableProps()} className="min-w-full divide-y divide-gray-200 dark:divide-gray-700"> <table {...getTableProps()} className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-800"> <thead className="bg-gray-50 dark:bg-gray-800">
{headerGroups.map((headerGroup) => { {headerGroups.map((headerGroup) => {
@ -269,7 +269,7 @@ export const ReleaseTable = () => {
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between"> <div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div className="flex items-baseline gap-x-2"> <div className="flex items-baseline gap-x-2">
<span className="text-sm text-gray-700"> <span className="text-sm text-gray-700">
Page <span className="font-medium">{pageIndex + 1}</span> of <span className="font-medium">{pageOptions.length}</span> Page <span className="font-medium">{pageIndex + 1}</span> of <span className="font-medium">{pageOptions.length}</span>
</span> </span>
<label> <label>
<span className="sr-only bg-gray-700">Items Per Page</span> <span className="sr-only bg-gray-700">Items Per Page</span>
@ -282,7 +282,7 @@ export const ReleaseTable = () => {
> >
{[5, 10, 20, 50].map(pageSize => ( {[5, 10, 20, 50].map(pageSize => (
<option key={pageSize} value={pageSize}> <option key={pageSize} value={pageSize}>
Show {pageSize} Show {pageSize}
</option> </option>
))} ))}
</select> </select>

View file

@ -17,6 +17,8 @@ interface ReleaseActionStatus {
status: string; status: string;
action: string; action: string;
type: string; type: string;
client: string;
filter: string;
rejections: string[]; rejections: string[];
timestamp: string timestamp: string
} }