mirror of
https://github.com/perstarkse/minne.git
synced 2026-03-25 10:52:07 +01:00
feat: displaying and managing active jobs
This commit is contained in:
284
assets/style.css
284
assets/style.css
@@ -828,6 +828,69 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.tab {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
webkit-user-select: none;
|
||||
user-select: none;
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
color: var(--color-base-content);
|
||||
}
|
||||
}
|
||||
--tab-p: 1rem;
|
||||
--tab-bg: var(--color-base-100);
|
||||
--tab-border-color: var(--color-base-300);
|
||||
--tab-radius-ss: 0;
|
||||
--tab-radius-se: 0;
|
||||
--tab-radius-es: 0;
|
||||
--tab-radius-ee: 0;
|
||||
--tab-order: 0;
|
||||
--tab-radius-min: calc(0.75rem - var(--border));
|
||||
border-color: transparent;
|
||||
order: var(--tab-order);
|
||||
height: calc(var(--size-field, 0.25rem) * 10);
|
||||
font-size: 0.875rem;
|
||||
padding-inline-start: var(--tab-p);
|
||||
padding-inline-end: var(--tab-p);
|
||||
&:is(input[type="radio"]) {
|
||||
min-width: fit-content;
|
||||
&:after {
|
||||
content: attr(aria-label);
|
||||
}
|
||||
}
|
||||
&:checked, &:is(.tab-active, [aria-selected="true"]) {
|
||||
& + .tab-content {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
&:not(:checked, :hover, .tab-active, [aria-selected="true"]) {
|
||||
color: color-mix(in oklab, var(--color-base-content) 50%, transparent);
|
||||
}
|
||||
&:not(input):empty {
|
||||
flex-grow: 1;
|
||||
cursor: default;
|
||||
}
|
||||
&:focus {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
&:focus-visible {
|
||||
outline: 2px solid currentColor;
|
||||
outline-offset: -5px;
|
||||
}
|
||||
&[disabled] {
|
||||
pointer-events: none;
|
||||
opacity: 40%;
|
||||
}
|
||||
}
|
||||
.dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
@@ -2343,6 +2406,25 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.tab-content {
|
||||
order {
|
||||
}
|
||||
order: var(--tabcontent-order);
|
||||
display: none;
|
||||
border-color: transparent;
|
||||
--tabcontent-radius-ss: 0;
|
||||
--tabcontent-radius-se: 0;
|
||||
--tabcontent-radius-es: 0;
|
||||
--tabcontent-radius-ee: 0;
|
||||
--tabcontent-order: 1;
|
||||
width: 100%;
|
||||
margin: var(--tabcontent-margin);
|
||||
border-width: var(--border);
|
||||
border-start-start-radius: var(--tabcontent-radius-ss);
|
||||
border-start-end-radius: var(--tabcontent-radius-se);
|
||||
border-end-start-radius: var(--tabcontent-radius-es);
|
||||
border-end-end-radius: var(--tabcontent-radius-ee);
|
||||
}
|
||||
.modal-box {
|
||||
grid-column-start: 1;
|
||||
grid-row-start: 1;
|
||||
@@ -2390,6 +2472,11 @@
|
||||
font-size: 2rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
.drawer-content {
|
||||
grid-column-start: 2;
|
||||
grid-row-start: 1;
|
||||
min-width: calc(0.25rem * 0);
|
||||
}
|
||||
.chat-image {
|
||||
grid-row: span 2 / span 2;
|
||||
align-self: flex-end;
|
||||
@@ -2427,6 +2514,15 @@
|
||||
max-width: 96rem;
|
||||
}
|
||||
}
|
||||
.m-0 {
|
||||
margin: calc(var(--spacing) * 0);
|
||||
}
|
||||
.m-6 {
|
||||
margin: calc(var(--spacing) * 6);
|
||||
}
|
||||
.m-8 {
|
||||
margin: calc(var(--spacing) * 8);
|
||||
}
|
||||
.filter {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -3026,6 +3122,9 @@
|
||||
.mt-8 {
|
||||
margin-top: calc(var(--spacing) * 8);
|
||||
}
|
||||
.mt-10 {
|
||||
margin-top: calc(var(--spacing) * 10);
|
||||
}
|
||||
.mr-1 {
|
||||
margin-right: calc(var(--spacing) * 1);
|
||||
}
|
||||
@@ -3048,6 +3147,9 @@
|
||||
.mb-8 {
|
||||
margin-bottom: calc(var(--spacing) * 8);
|
||||
}
|
||||
.mb-10 {
|
||||
margin-bottom: calc(var(--spacing) * 10);
|
||||
}
|
||||
.alert {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
@@ -3319,6 +3421,13 @@
|
||||
column-gap: calc(0.25rem * 3);
|
||||
padding-block: calc(0.25rem * 1);
|
||||
}
|
||||
.mask {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
mask-size: contain;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
}
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
@@ -3352,6 +3461,27 @@
|
||||
.table {
|
||||
display: table;
|
||||
}
|
||||
.btn-square {
|
||||
padding-inline: calc(0.25rem * 0);
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
}
|
||||
.size-6 {
|
||||
width: calc(var(--spacing) * 6);
|
||||
height: calc(var(--spacing) * 6);
|
||||
}
|
||||
.size-8 {
|
||||
width: calc(var(--spacing) * 8);
|
||||
height: calc(var(--spacing) * 8);
|
||||
}
|
||||
.size-10 {
|
||||
width: calc(var(--spacing) * 10);
|
||||
height: calc(var(--spacing) * 10);
|
||||
}
|
||||
.size-\[1\.2em\] {
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
}
|
||||
.h-5 {
|
||||
height: calc(var(--spacing) * 5);
|
||||
}
|
||||
@@ -3367,12 +3497,6 @@
|
||||
.w-5 {
|
||||
width: calc(var(--spacing) * 5);
|
||||
}
|
||||
.w-12 {
|
||||
width: calc(var(--spacing) * 12);
|
||||
}
|
||||
.w-24 {
|
||||
width: calc(var(--spacing) * 24);
|
||||
}
|
||||
.w-32 {
|
||||
width: calc(var(--spacing) * 32);
|
||||
}
|
||||
@@ -3388,6 +3512,9 @@
|
||||
.flex-shrink {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
.shrink {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
.flex-grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
@@ -3463,15 +3590,24 @@
|
||||
.grid-cols-1 {
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
}
|
||||
.\!flex-col {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
.flex-row {
|
||||
flex-direction: row;
|
||||
}
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -3481,6 +3617,9 @@
|
||||
.justify-start {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.gap-1 {
|
||||
gap: calc(var(--spacing) * 1);
|
||||
}
|
||||
.gap-4 {
|
||||
gap: calc(var(--spacing) * 4);
|
||||
}
|
||||
@@ -3546,12 +3685,21 @@
|
||||
.border-transparent {
|
||||
border-color: transparent;
|
||||
}
|
||||
.bg-accent {
|
||||
background-color: var(--color-accent);
|
||||
}
|
||||
.bg-base-100 {
|
||||
background-color: var(--color-base-100);
|
||||
}
|
||||
.bg-base-200 {
|
||||
background-color: var(--color-base-200);
|
||||
}
|
||||
.bg-primary {
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
.bg-secondary {
|
||||
background-color: var(--color-secondary);
|
||||
}
|
||||
.bg-linear-to-r {
|
||||
--tw-gradient-position: to right in oklab,;
|
||||
background-image: linear-gradient(var(--tw-gradient-stops));
|
||||
@@ -3619,6 +3767,9 @@
|
||||
.pt-10 {
|
||||
padding-top: calc(var(--spacing) * 10);
|
||||
}
|
||||
.pb-2 {
|
||||
padding-bottom: calc(var(--spacing) * 2);
|
||||
}
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -3672,6 +3823,10 @@
|
||||
--tw-font-weight: var(--font-weight-semibold);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
.tracking-wide {
|
||||
--tw-tracking: var(--tracking-wide);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
}
|
||||
.text-nowrap {
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
@@ -3693,6 +3848,9 @@
|
||||
.text-accent {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
.text-accent-content {
|
||||
color: var(--color-accent-content);
|
||||
}
|
||||
.text-base-content {
|
||||
color: var(--color-base-content);
|
||||
}
|
||||
@@ -3717,6 +3875,9 @@
|
||||
.text-secondary {
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
.text-secondary-content {
|
||||
color: var(--color-secondary-content);
|
||||
}
|
||||
.text-transparent {
|
||||
color: transparent;
|
||||
}
|
||||
@@ -3729,6 +3890,10 @@
|
||||
.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
.ordinal {
|
||||
--tw-ordinal: ordinal;
|
||||
font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);
|
||||
}
|
||||
.btn-link {
|
||||
text-decoration-line: underline;
|
||||
outline-color: currentColor;
|
||||
@@ -3746,6 +3911,9 @@
|
||||
.underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.accent-accent-content {
|
||||
accent-color: var(--color-accent-content);
|
||||
}
|
||||
.opacity-60 {
|
||||
opacity: 60%;
|
||||
}
|
||||
@@ -3757,10 +3925,6 @@
|
||||
opacity: 100%;
|
||||
}
|
||||
}
|
||||
.ring {
|
||||
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentColor);
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
.shadow {
|
||||
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
@@ -3866,6 +4030,11 @@
|
||||
margin-top: calc(var(--spacing) * 4);
|
||||
}
|
||||
}
|
||||
.sm\:w-fit {
|
||||
@media (width >= 40rem) {
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
.sm\:max-w-md {
|
||||
@media (width >= 40rem) {
|
||||
max-width: var(--container-md);
|
||||
@@ -3876,6 +4045,16 @@
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
.sm\:flex-col {
|
||||
@media (width >= 40rem) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
.sm\:flex-row {
|
||||
@media (width >= 40rem) {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
.sm\:px-0 {
|
||||
@media (width >= 40rem) {
|
||||
padding-inline: calc(var(--spacing) * 0);
|
||||
@@ -3887,6 +4066,11 @@
|
||||
line-height: var(--tw-leading, var(--text-6xl--line-height));
|
||||
}
|
||||
}
|
||||
.md\:grid-cols-2 {
|
||||
@media (width >= 48rem) {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
.md\:grid-cols-3 {
|
||||
@media (width >= 48rem) {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
@@ -3907,6 +4091,50 @@
|
||||
grid-template-columns: auto 1fr;
|
||||
}
|
||||
}
|
||||
.\[\&\:before\]\:normal-case {
|
||||
&:before {
|
||||
text-transform: none;
|
||||
}
|
||||
}
|
||||
.\[\&\:before\]\:uppercase {
|
||||
&:before {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
.\[\&\:before\]\:opacity-60 {
|
||||
&:before {
|
||||
opacity: 60%;
|
||||
}
|
||||
}
|
||||
.\[\&\:before\]\:opacity-100 {
|
||||
&:before {
|
||||
opacity: 100%;
|
||||
}
|
||||
}
|
||||
.\[\&\:before\]\:content-\[\'Content\:_\'\] {
|
||||
&:before {
|
||||
--tw-content: 'Content: ';
|
||||
content: var(--tw-content);
|
||||
}
|
||||
}
|
||||
.\[\&\:before\]\:content-\[\'Instructions\:_\'\] {
|
||||
&:before {
|
||||
--tw-content: 'Instructions: ';
|
||||
content: var(--tw-content);
|
||||
}
|
||||
}
|
||||
.\[\&\:before\]\:content-\[\'Status\:_\'\] {
|
||||
&:before {
|
||||
--tw-content: 'Status: ';
|
||||
content: var(--tw-content);
|
||||
}
|
||||
}
|
||||
.\[\&\:before\]\:content-\[\'Text\:_\'\] {
|
||||
&:before {
|
||||
--tw-content: 'Text: ';
|
||||
content: var(--tw-content);
|
||||
}
|
||||
}
|
||||
}
|
||||
@layer base {
|
||||
*, ::after, ::before, ::backdrop, ::file-selector-button {
|
||||
@@ -4262,6 +4490,12 @@ video {
|
||||
--tw-gradient-via-position: 50%;
|
||||
--tw-gradient-to-position: 100%;
|
||||
--tw-font-weight: initial;
|
||||
--tw-tracking: initial;
|
||||
--tw-ordinal: initial;
|
||||
--tw-slashed-zero: initial;
|
||||
--tw-numeric-figure: initial;
|
||||
--tw-numeric-spacing: initial;
|
||||
--tw-numeric-fraction: initial;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-color: initial;
|
||||
--tw-inset-shadow: 0 0 #0000;
|
||||
@@ -4294,6 +4528,7 @@ video {
|
||||
--tw-backdrop-saturate: initial;
|
||||
--tw-backdrop-sepia: initial;
|
||||
--tw-ease: initial;
|
||||
--tw-content: "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4404,6 +4639,30 @@ video {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-tracking {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-ordinal {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-slashed-zero {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-numeric-figure {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-numeric-spacing {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-numeric-fraction {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-shadow {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
@@ -4540,3 +4799,8 @@ video {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-content {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: "";
|
||||
}
|
||||
|
||||
@@ -31,9 +31,8 @@ use zettle_db::{
|
||||
admin_panel::{show_admin_panel, toggle_registration_status},
|
||||
documentation::index::show_documentation_index,
|
||||
gdpr::{accept_gdpr, deny_gdpr},
|
||||
index::index_handler,
|
||||
ingress_form::{process_ingress_form, show_ingress_form},
|
||||
ingress_tasks::{delete_task, show_queue_tasks},
|
||||
index::{delete_job, delete_text_content, index_handler},
|
||||
ingress_form::{hide_ingress_form, process_ingress_form, show_ingress_form},
|
||||
privacy_policy::show_privacy_policy,
|
||||
search_result::search_result_handler,
|
||||
signin::{authenticate_user, show_signin_form},
|
||||
@@ -168,8 +167,9 @@ fn html_routes(
|
||||
"/ingress-form",
|
||||
get(show_ingress_form).post(process_ingress_form),
|
||||
)
|
||||
.route("/queue", get(show_queue_tasks))
|
||||
.route("/queue/:delivery_tag", delete(delete_task))
|
||||
.route("/hide-ingress-form", get(hide_ingress_form))
|
||||
.route("/text-content/:id", delete(delete_text_content))
|
||||
.route("/jobs/:job_id", delete(delete_job))
|
||||
.route("/account", get(show_account_page))
|
||||
.route("/admin", get(show_admin_panel))
|
||||
.route("/toggle-registrations", patch(toggle_registration_status))
|
||||
|
||||
@@ -57,6 +57,31 @@ impl JobQueue {
|
||||
Ok(jobs)
|
||||
}
|
||||
|
||||
/// Gets all active jobs for a specific user
|
||||
pub async fn get_unfinished_user_jobs(&self, user_id: &str) -> Result<Vec<Job>, AppError> {
|
||||
let jobs: Vec<Job> = self
|
||||
.db
|
||||
.query(
|
||||
"SELECT * FROM type::table($table)
|
||||
WHERE user_id = $user_id
|
||||
AND (
|
||||
status = 'Created'
|
||||
OR (
|
||||
status.InProgress != NONE
|
||||
AND status.InProgress.attempts < $max_attempts
|
||||
)
|
||||
)
|
||||
ORDER BY created_at DESC",
|
||||
)
|
||||
.bind(("table", Job::table_name()))
|
||||
.bind(("user_id", user_id.to_owned()))
|
||||
.bind(("max_attempts", MAX_ATTEMPTS))
|
||||
.await?
|
||||
.take(0)?;
|
||||
debug!("{:?}", jobs);
|
||||
Ok(jobs)
|
||||
}
|
||||
|
||||
pub async fn delete_job(&self, id: &str, user_id: &str) -> Result<(), AppError> {
|
||||
get_item::<Job>(&self.db.client, id)
|
||||
.await?
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use axum::{extract::State, response::IntoResponse};
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
response::{IntoResponse, Redirect},
|
||||
};
|
||||
use axum_session::Session;
|
||||
use axum_session_auth::AuthSession;
|
||||
use axum_session_surreal::SessionSurrealPool;
|
||||
@@ -6,17 +9,23 @@ use surrealdb::{engine::any::Any, Surreal};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
error::HtmlError,
|
||||
error::{AppError, HtmlError},
|
||||
page_data,
|
||||
server::{routes::html::render_template, AppState},
|
||||
storage::types::{text_content::TextContent, user::User},
|
||||
server::{
|
||||
routes::html::{render_block, render_template},
|
||||
AppState,
|
||||
},
|
||||
storage::{
|
||||
db::delete_item,
|
||||
types::{job::Job, text_content::TextContent, user::User},
|
||||
},
|
||||
};
|
||||
|
||||
page_data!(IndexData, "index/index.html", {
|
||||
gdpr_accepted: bool,
|
||||
queue_length: u32,
|
||||
user: Option<User>,
|
||||
latest_text_contents: Vec<TextContent>
|
||||
latest_text_contents: Vec<TextContent>,
|
||||
active_jobs: Vec<Job>
|
||||
});
|
||||
|
||||
pub async fn index_handler(
|
||||
@@ -28,17 +37,16 @@ pub async fn index_handler(
|
||||
|
||||
let gdpr_accepted = auth.current_user.is_some() | session.get("gdpr_accepted").unwrap_or(false);
|
||||
|
||||
let queue_length = match auth.current_user.is_some() {
|
||||
let active_jobs = match auth.current_user.is_some() {
|
||||
true => state
|
||||
.job_queue
|
||||
.get_user_jobs(&auth.current_user.clone().unwrap().id)
|
||||
.get_unfinished_user_jobs(&auth.current_user.clone().unwrap().id)
|
||||
.await
|
||||
.map_err(|e| HtmlError::new(e, state.templates.clone()))?
|
||||
.len(),
|
||||
false => 0,
|
||||
.map_err(|e| HtmlError::new(e, state.templates.clone()))?,
|
||||
false => vec![],
|
||||
};
|
||||
|
||||
let latest_text_contents = match auth.current_user.is_some() {
|
||||
let latest_text_contents = match auth.current_user.clone().is_some() {
|
||||
true => User::get_latest_text_contents(
|
||||
auth.current_user.clone().unwrap().id.as_str(),
|
||||
&state.surreal_db_client,
|
||||
@@ -65,10 +73,90 @@ pub async fn index_handler(
|
||||
let output = render_template(
|
||||
IndexData::template_name(),
|
||||
IndexData {
|
||||
queue_length: queue_length.try_into().unwrap(),
|
||||
gdpr_accepted,
|
||||
user: auth.current_user,
|
||||
latest_text_contents,
|
||||
active_jobs,
|
||||
},
|
||||
state.templates.clone(),
|
||||
)?;
|
||||
|
||||
Ok(output.into_response())
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct LatestTextContentData {
|
||||
latest_text_contents: Vec<TextContent>,
|
||||
user: User,
|
||||
}
|
||||
|
||||
pub async fn delete_text_content(
|
||||
State(state): State<AppState>,
|
||||
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<impl IntoResponse, HtmlError> {
|
||||
let user = match &auth.current_user {
|
||||
Some(user) => user,
|
||||
None => return Ok(Redirect::to("/").into_response()),
|
||||
};
|
||||
|
||||
delete_item::<TextContent>(&state.surreal_db_client, &id)
|
||||
.await
|
||||
.map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?;
|
||||
|
||||
let latest_text_contents = User::get_latest_text_contents(&user.id, &state.surreal_db_client)
|
||||
.await
|
||||
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
|
||||
|
||||
info!("{:?}", latest_text_contents);
|
||||
|
||||
let output = render_block(
|
||||
"index/signed_in/recent_content.html",
|
||||
"latest_content_section",
|
||||
LatestTextContentData {
|
||||
user: user.clone(),
|
||||
latest_text_contents,
|
||||
},
|
||||
state.templates.clone(),
|
||||
)?;
|
||||
|
||||
Ok(output.into_response())
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ActiveJobsData {
|
||||
active_jobs: Vec<Job>,
|
||||
user: User,
|
||||
}
|
||||
|
||||
pub async fn delete_job(
|
||||
State(state): State<AppState>,
|
||||
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<impl IntoResponse, HtmlError> {
|
||||
let user = match auth.current_user {
|
||||
Some(user) => user,
|
||||
None => return Ok(Redirect::to("/signin").into_response()),
|
||||
};
|
||||
|
||||
state
|
||||
.job_queue
|
||||
.delete_job(&id, &user.id)
|
||||
.await
|
||||
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
|
||||
|
||||
let active_jobs = state
|
||||
.job_queue
|
||||
.get_unfinished_user_jobs(&user.id)
|
||||
.await
|
||||
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
|
||||
|
||||
let output = render_block(
|
||||
"index/signed_in/active_jobs.html",
|
||||
"active_jobs_section",
|
||||
ActiveJobsData {
|
||||
user: user.clone(),
|
||||
active_jobs,
|
||||
},
|
||||
state.templates.clone(),
|
||||
)?;
|
||||
|
||||
@@ -33,6 +33,19 @@ pub async fn show_ingress_form(
|
||||
Ok(output.into_response())
|
||||
}
|
||||
|
||||
pub async fn hide_ingress_form(
|
||||
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
|
||||
) -> Result<impl IntoResponse, HtmlError> {
|
||||
if !auth.is_authenticated() {
|
||||
return Ok(Redirect::to("/").into_response());
|
||||
}
|
||||
|
||||
Ok(Html(
|
||||
"<a class='btn btn-primary' hx-get='/ingress-form' hx-swap='outerHTML'>Add Content</a>",
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
#[derive(Debug, TryFromMultipart)]
|
||||
pub struct IngressParams {
|
||||
pub content: Option<String>,
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
use crate::{
|
||||
error::HtmlError,
|
||||
page_data,
|
||||
server::AppState,
|
||||
storage::types::{job::Job, user::User},
|
||||
};
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
response::{Html, IntoResponse, Redirect},
|
||||
};
|
||||
use axum_session_auth::AuthSession;
|
||||
use axum_session_surreal::SessionSurrealPool;
|
||||
use surrealdb::{engine::any::Any, Surreal};
|
||||
|
||||
use super::render_template;
|
||||
|
||||
page_data!(ShowQueueTasks, "queue_tasks.html", {user : User,jobs: Vec<Job>});
|
||||
|
||||
pub async fn show_queue_tasks(
|
||||
State(state): State<AppState>,
|
||||
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
|
||||
) -> Result<impl IntoResponse, HtmlError> {
|
||||
let user = match auth.current_user {
|
||||
Some(user) => user,
|
||||
None => return Ok(Redirect::to("/signin").into_response()),
|
||||
};
|
||||
|
||||
let jobs = state
|
||||
.job_queue
|
||||
.get_user_jobs(&user.id)
|
||||
.await
|
||||
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
|
||||
|
||||
let rendered = render_template(
|
||||
ShowQueueTasks::template_name(),
|
||||
ShowQueueTasks { jobs, user },
|
||||
state.templates.clone(),
|
||||
)?;
|
||||
|
||||
Ok(rendered.into_response())
|
||||
}
|
||||
|
||||
pub async fn delete_task(
|
||||
State(state): State<AppState>,
|
||||
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<impl IntoResponse, HtmlError> {
|
||||
let user = match auth.current_user {
|
||||
Some(user) => user,
|
||||
None => return Ok(Redirect::to("/signin").into_response()),
|
||||
};
|
||||
|
||||
state
|
||||
.job_queue
|
||||
.delete_job(&id, &user.id)
|
||||
.await
|
||||
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
|
||||
|
||||
Ok(Html("").into_response())
|
||||
}
|
||||
@@ -11,7 +11,6 @@ pub mod documentation;
|
||||
pub mod gdpr;
|
||||
pub mod index;
|
||||
pub mod ingress_form;
|
||||
pub mod ingress_tasks;
|
||||
pub mod privacy_policy;
|
||||
pub mod search_result;
|
||||
pub mod signin;
|
||||
|
||||
46
templates/index/signed_in/active_jobs.html
Normal file
46
templates/index/signed_in/active_jobs.html
Normal file
@@ -0,0 +1,46 @@
|
||||
{% block active_jobs_section %}
|
||||
{% if active_jobs %}
|
||||
<ul id="active_jobs_section" class="list bg-base-100 rounded-box shadow-md">
|
||||
<li class="p-4 pb-2 text-xs opacity-60 tracking-wide">Active Jobs</li>
|
||||
{% for item in active_jobs %}
|
||||
<li class="list-row">
|
||||
<div class="bg-secondary rounded-box size-10 flex justify-center items-center text-secondary-content">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
|
||||
class="size-8">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
{{item.created_at|datetimeformat(format="short", tz=user.timezone)}}</div>
|
||||
<div
|
||||
class="text-xs font-semibold opacity-60 [&:before]:content-['Status:_'] [&:before]:uppercase [&:before]:opacity-60">
|
||||
{{item.status}}
|
||||
</div>
|
||||
</div>
|
||||
<p class="list-col-wrap text-xs [&:before]:content-['Content:_'] [&:before]:uppercase [&:before]:opacity-60">
|
||||
{{item.content}}
|
||||
</p>
|
||||
<!-- <button class="btn disabled btn-square btn-ghost"> -->
|
||||
<!-- <svg class="size-[1.2em]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> -->
|
||||
<!-- <g stroke-linejoin="round" stroke-linecap="round" stroke-width="2" fill="none" stroke="currentColor"> -->
|
||||
<!-- <path d="M20.71 7.04c.39-.39.39-1.04 0-1.41l-2.34-2.34c-.37-.39-1.02-.39-1.41 0l-1.84 1.83 3.75 3.75z"></path> -->
|
||||
<!-- <path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75z"></path> -->
|
||||
<!-- </g> -->
|
||||
<!-- </svg> -->
|
||||
<!-- </button> -->
|
||||
<button hx-delete="/jobs/{{item.id}}" hx-target="#active_jobs_section" hx-swap="outerHTML"
|
||||
class="btn btn-square btn-ghost">
|
||||
<svg class="size-[1.2em]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g stroke-linejoin="round" stroke-linecap="round" stroke-width="2" fill="none" stroke="currentColor">
|
||||
<path d="M3 6h18"></path>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -2,8 +2,13 @@
|
||||
<div class="container">
|
||||
{% include 'index/signed_in/searchbar.html' %}
|
||||
|
||||
{% include "index/signed_in/recent_content.html" %}
|
||||
|
||||
{% include "index/signed_in/quick_actions.html" %}
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2">
|
||||
{% include "index/signed_in/active_jobs.html" %}
|
||||
|
||||
{% include "index/signed_in/recent_content.html" %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="card bg-base-100 shadow-xl mt-4">
|
||||
<div class="card-body">
|
||||
<div class="flex gap-4">
|
||||
<a class="btn btn-primary" hx-get="/ingress-form" hx-swap="outerHTML">Add Content</a>
|
||||
<button class="btn btn-primary" hx-get="/ingress-form" hx-swap="outerHTML">Add Content</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,24 +1,45 @@
|
||||
<div class="card bg-base-100 shadow-xl mt-4">
|
||||
<!-- <div class="mt-4"> -->
|
||||
<p>{{latest_text_contents}}</p>
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Recently Added Content</h2>
|
||||
<ul class="list bg-base-100 rounded-box shadow-md">
|
||||
{% for item in latest_text_contents %}
|
||||
<li class="list-row">
|
||||
<div class="text-2xl text-ellipsis text-nowrap overflow-hidden">
|
||||
{{item.created_at|datetimeformat(format="short", tz="Europe/Stockholm")}}</div>
|
||||
<div>
|
||||
<div>{{item.category}}</div>
|
||||
<div class="text-xs uppercase font-semibold opacity-60">{{item.text}}</div>
|
||||
</div>
|
||||
<button class="btn btn-outline">
|
||||
Edit
|
||||
</button>
|
||||
<button class="btn btn-error">
|
||||
Delete </button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% block latest_content_section %}
|
||||
<ul id="latest_content_section" class="list bg-base-100 rounded-box shadow-md">
|
||||
<li class="p-4 pb-2 text-xs opacity-60 tracking-wide">Recently added content</li>
|
||||
{% for item in latest_text_contents %}
|
||||
<li class="list-row">
|
||||
<div class="bg-accent rounded-box size-10 flex justify-center items-center text-accent-content">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
|
||||
class="size-8">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
{{item.created_at|datetimeformat(format="short", tz=user.timezone)}}</div>
|
||||
<div
|
||||
class="text-xs font-semibold opacity-60 [&:before]:content-['Instructions:_'] [&:before]:uppercase [&:before]:opacity-60">
|
||||
{{item.instructions}}
|
||||
</div>
|
||||
</div>
|
||||
<p class="list-col-wrap text-xs [&:before]:content-['Content:_'] [&:before]:uppercase [&:before]:opacity-60">
|
||||
{{item.text}}
|
||||
</p>
|
||||
<!-- <button class="btn disabled btn-square btn-ghost"> -->
|
||||
<!-- <svg class="size-[1.2em]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> -->
|
||||
<!-- <g stroke-linejoin="round" stroke-linecap="round" stroke-width="2" fill="none" stroke="currentColor"> -->
|
||||
<!-- <path d="M20.71 7.04c.39-.39.39-1.04 0-1.41l-2.34-2.34c-.37-.39-1.02-.39-1.41 0l-1.84 1.83 3.75 3.75z"></path> -->
|
||||
<!-- <path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75z"></path> -->
|
||||
<!-- </g> -->
|
||||
<!-- </svg> -->
|
||||
<!-- </button> -->
|
||||
<button hx-delete="/text-content/{{item.id}}" hx-target="#latest_content_section" hx-swap="outerHTML"
|
||||
class="btn btn-square btn-ghost">
|
||||
<svg class="size-[1.2em]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g stroke-linejoin="round" stroke-linecap="round" stroke-width="2" fill="none" stroke="currentColor">
|
||||
<path d="M3 6h18"></path>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
@@ -1,4 +1,4 @@
|
||||
<form class="space-y-4 mt-2 w-full" hx-post="/ingress-form" enctype="multipart/form-data">
|
||||
<form id="ingress-form" class="space-y-4 mt-2 w-full" hx-post="/ingress-form" enctype="multipart/form-data">
|
||||
<div class="form-control">
|
||||
<label class="floating-label">
|
||||
<span>Instructions</span>
|
||||
@@ -34,7 +34,9 @@
|
||||
|
||||
<div id="error-message" class="text-error text-center {% if not error %}hidden{% endif %}">{{ error }}</div>
|
||||
|
||||
<div class="form-control mt-6">
|
||||
<button type="submit" class="btn btn-primary w-full">Submit</button>
|
||||
<div class="form-control mt-6 flex flex-col sm:flex-row gap-1">
|
||||
<button hx-get="/hide-ingress-form" hx-target="#ingress-form" hx-swap=outerHTML"
|
||||
class="btn btn-outline w-full sm:w-fit">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary w-full sm:w-fit">Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,62 +0,0 @@
|
||||
{% extends "body_base.html" %}
|
||||
{% block main %}
|
||||
<div class="container mx-auto p-4">
|
||||
<h1 class="text-2xl font-bold mb-4">Active Tasks</h1>
|
||||
{% if not jobs %}
|
||||
<div class="alert alert-info">
|
||||
<span>No active tasks</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="grid gap-4">
|
||||
{% for job in jobs %}
|
||||
{% if job.status == "Created" or job.status is mapping and job.status.InProgress %}
|
||||
<div class="card bg-base-200 shadow-xl" id="job-card-{{ job.id }}">
|
||||
<div class="card-body">
|
||||
<div class="card-title">
|
||||
{% if job.content.Url %}
|
||||
<h2>URL Task</h2>
|
||||
<p class="text-sm text-gray-500 break-all">{{ job.content.Url.url }}</p>
|
||||
{% elif job.content.File %}
|
||||
<h2>File Task</h2>
|
||||
<p class="text-sm text-gray-500">{{ job.content.File.file_info.path }}</p>
|
||||
{% elif job.content.Text %}
|
||||
<h2>Text Task</h2>
|
||||
<p class="text-sm text-gray-500">{{ job.content.Text.text }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<p><span class="font-medium">Status:</span>
|
||||
{% if job.status == "Created" %}
|
||||
Created
|
||||
{% elif job.status.InProgress %}
|
||||
In Progress
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if job.status.InProgress %}
|
||||
<p><span class="font-medium">Attempts:</span> {{ job.status.InProgress.attempts }}</p>
|
||||
<p><span class="font-medium">Last Attempt:</span>
|
||||
{{ job.status.InProgress.last_attempt }}</p>
|
||||
{% endif %}
|
||||
<p><span class="font-medium">Category:</span>
|
||||
{% if job.content.Url %}
|
||||
{{ job.content.Url.category }}
|
||||
{% elif job.content.File %}
|
||||
{{ job.content.File.category }}
|
||||
{% elif job.content.Text %}
|
||||
{{ job.content.Text.category }}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-actions justify-end mt-4">
|
||||
<form hx-delete="/queue/{{ job.id }}" hx-target="#job-card-{{ job.id }}" hx-swap="outerHTML">
|
||||
<button class="btn btn-error">Cancel Task</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user