Merge pull request #409 from storytold/brandon-kill-jobs-panel

Moderation panel to kill generic jobs
This commit is contained in:
Brandon Thomas authored and GitHub committed 2023-12-04 05:15:08 -05:00
commit 7cfd3f08c2
5 files changed
+281 -1

No files matched your search

+5
View File
@@ -70,6 +70,7 @@ class ApiConfig {
break;
case "dev.fakeyou.com:7000":
case "dev.fakeyou.com:7001":
case "dev.fakeyou.com:7002":
// NB: for dev machines without nginx proxies
apiHost = "api.dev.fakeyou.com:12345";
useSsl = false;
@@ -390,6 +391,10 @@ class ApiConfig {
return `${this.getApiOrigin()}/moderation/jobs/kill_tts_inference_jobs`;
}
killJobs(): string {
return `${this.getApiOrigin()}/moderation/jobs/kill_generic`;
}
getW2lInferenceStats(): string {
return `${this.getApiOrigin()}/moderation/jobs/w2l_inference_queue_stats`;
}
@@ -0,0 +1,54 @@
import { ApiConfig } from "@storyteller/components";
export interface KillJobsRequest {
job_statuses: string[],
}
export interface KillJobsSuccessResponse {
success: boolean,
}
export interface KillJobsErrorResponse {
success: boolean,
}
type KillJobsResponse = KillJobsSuccessResponse | KillJobsErrorResponse;
export function KillJobsIsSuccess(response: KillJobsResponse): response is KillJobsSuccessResponse {
return response?.success === true;
}
export function KillJobsIsError(response: KillJobsResponse): response is KillJobsErrorResponse {
return response?.success === false;
}
export async function KillJobs(request: KillJobsRequest) : Promise<KillJobsResponse>
{
const endpoint = new ApiConfig().killJobs();
return await fetch(endpoint, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify(request),
})
.then(res => res.json())
.then(res => {
if (!res) {
return { success : false };
}
if (res && 'success' in res) {
return res;
} else {
return { success : false };
}
})
.catch(e => {
return { success : false };
});
}
@@ -97,6 +97,7 @@ import { PasswordResetEmailPage } from "./pages/password_reset/PasswordResetEmai
import { PasswordResetVerificationPage } from "./pages/password_reset/PasswordResetVerificationPage";
import { NewProfilePage } from "./pages/profile/profile_view/NewProfilePage";
import { ProfilePage } from "./pages/profile/profile_view/ProfilePage";
import { ModerationJobControlPage } from "./pages/moderation/job_control/ModerationJobControlPage";
interface Props {
sessionWrapper: SessionWrapper;
@@ -530,6 +531,12 @@ class PageContainer extends React.Component<
/>
</Route>
<Route path="/moderation/job_control">
<ModerationJobControlPage
sessionWrapper={this.props.sessionWrapper}
/>
</Route>
<Route path="/moderation/tts_category/list">
<ModerationTtsCategoryListPage
sessionWrapper={this.props.sessionWrapper}
@@ -0,0 +1,205 @@
import React, { useState } from "react";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import { WebUrl } from "../../../../../common/WebUrl";
import { BackLink } from "../../../_common/BackLink";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faBomb } from "@fortawesome/free-solid-svg-icons";
import {
KillJobs,
} from "@storyteller/components/src/api/moderation/queues/KillJobs";
interface Props {
sessionWrapper: SessionWrapper;
}
function ModerationJobControlPage(props: Props) {
const [killPendingJobs, setKillPendingJobs] = useState<boolean>(true);
const [killFailedJobs, setKillFailedJobs] = useState<boolean>(true);
const [killStartedJobs, setKillStartedJobs] = useState<boolean>(true);
const [priorityLevel, setPriorityLevel] = useState<number>(0);
const [target, setTarget] = useState<string>('all_jobs');
const killJobs= async () => {
let jobStatuses = [];
if (killPendingJobs) { jobStatuses.push('pending') }
if (killFailedJobs) { jobStatuses.push('failed') }
if (killStartedJobs) { jobStatuses.push('started') }
// NB: The backend uses parameterized enums, so the request shape here is unusual
let requestTarget: any = 'all_jobs';
switch (target) {
case 'all_jobs':
break;
case 'lipsync_animation':
case 'text_to_speech':
case 'voice_conversion':
requestTarget = { 'category': target };
break;
case 'rvc_v2':
case 'so_vits_svc':
case 'tt2':
requestTarget = { 'model_type': target };
break;
}
const request = {
job_statuses: jobStatuses,
target: requestTarget,
maybe_priority_or_lower: priorityLevel,
};
await KillJobs(request);
};
const checkboxEventStatus = (
ev: React.FormEvent<HTMLInputElement>
) : boolean => {
return (ev.target as HTMLInputElement).checked;
}
const handlePriorityChange = (ev: React.FormEvent<HTMLInputElement>) => {
const level = (ev.target as HTMLInputElement).value;
let numericLevel = parseInt(level.trim());
if (isNaN(numericLevel)) {
numericLevel = 0;
} else if (numericLevel < 0) {
numericLevel = 0;
}
setPriorityLevel(numericLevel);
};
const handleTargetChange = (ev: React.FormEvent<HTMLSelectElement>) => {
const value = (ev.target as HTMLSelectElement).value;
setTarget(value);
};
if (!props.sessionWrapper.canBanUsers()) {
return <h1>Unauthorized</h1>;
}
return (
<div>
<div className="container py-5">
<h1 className=" fw-bold">Job Stats</h1>
<div className="pt-3">
<BackLink link={WebUrl.moderationMain()} text="Back to moderation" />
</div>
</div>
<div className="container-panel pt-3 pb-5">
<div className="panel p-3 p-lg-4">
<h2 className="panel-title fw-bold">Kill Jobs (Danger Zone)</h2>
<h5 className="fw-semibold mb-4">(Only do this in emergencies!)</h5>
<h5 className="fw-semibold mb-4">Job Types / Categories</h5>
<div>
<label className="sub-title">Category Type</label>
<div className="form-group">
<select
onChange={handleTargetChange}
className="form-select"
value={target}
>
<option value="all_jobs">ALL JOBS !!!</option>
<option value="lipsync_animation">Category: Lipsync Animation</option>
<option value="text_to_speech">Category: Text-to-Speech (TT2, Voice Designer, etc.)</option>
<option value="voice_conversion">Category: Voice Conversion (RVC, SVC, etc.)</option>
<option value="rvc_v2">Model type: RVCv2</option>
<option value="so_vits_svc">Model type: SVC</option>
<option value="tt2">Model type: TT2</option>
</select>
</div>
<br />
</div>
<h5 className="fw-semibold mb-4">Job Statuses</h5>
<div className="d-flex flex-column gap-3">
<label className="form-check-label">
<input
type="checkbox"
checked={killFailedJobs}
onChange={(ev) => setKillFailedJobs(checkboxEventStatus(ev))}
className="form-check-input"
/>
&nbsp; Kill Failed Jobs
</label>
</div>
<div className="d-flex flex-column gap-3">
<label className="form-check-label">
<input
type="checkbox"
checked={killPendingJobs}
onChange={(ev) => setKillPendingJobs(checkboxEventStatus(ev))}
className="form-check-input"
/>
&nbsp; Kill Pending Jobs
</label>
</div>
<div className="d-flex flex-column gap-3">
<label className="form-check-label">
<input
type="checkbox"
checked={killStartedJobs}
onChange={(ev) => setKillStartedJobs(checkboxEventStatus(ev))}
className="form-check-input"
/>
&nbsp; Kill Started Jobs
</label>
</div>
<br />
<h5 className="fw-semibold mb-4">Job Priorities</h5>
<div>
<label className="sub-title">Kill Priority Level(s) and Below</label>
<div className="form-group">
<input
className="form-control"
type="text"
placeholder="Priority Level"
value={priorityLevel}
onChange={handlePriorityChange}
/>
</div>
<br />
<div>eg. setting this to "3" will kill all priorities 3 and below (3, 2, 1, and 0).</div>
<br />
<ul>
<li>Level 0 = logged out</li>
<li>Level 1 = free account</li>
<li>Level 2+ = premium or loyalty user</li>
</ul>
<br />
<div>Try not to kill premium user workloads unless things get bad.</div>
</div>
<div className="py-6">
<div className="d-flex flex-column gap-3">
<button
className="btn btn-destructive w-100"
onClick={() => killJobs()}
>
Kill Jobs&nbsp;
<FontAwesomeIcon icon={faBomb} />
</button>
</div>
</div>
</div>
</div>
<div className="container pb-5">
<BackLink link={WebUrl.moderationMain()} text="Back to moderation" />
</div>
</div>
);
}
export { ModerationJobControlPage };
@@ -38,12 +38,21 @@ function ModerationFc(props: Props) {
<FontAwesomeIcon icon={faBan} className="me-2" />
IP Bans
</Link>
<Link
to="/moderation/job_control"
className="btn btn-primary w-100"
>
<FontAwesomeIcon icon={faListCheck} className="me-2" />
Job Control (New)
</Link>
<Link
to="/moderation/job_stats"
className="btn btn-primary w-100"
>
<FontAwesomeIcon icon={faListCheck} className="me-2" />
Job Stats
Job Stats (Old TTS + W2L)
</Link>
</div>
</div>