[ENG-941] Jobs running in another library do not appear in the job manager (#1306)

* Fetching job reports from all libraries
Some clippy warnings
pnpm format

* Reverting expects to unwraps
This commit is contained in:
Ericson "Fogo" Soares 2023-09-07 01:15:31 -03:00 committed by GitHub
parent 5860016789
commit f8033d1842
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 139 additions and 103 deletions

View file

@ -1,7 +1,7 @@
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { forwardRef } from 'react'; import { forwardRef } from 'react';
import { FlatList, Text, View } from 'react-native'; import { FlatList, Text, View } from 'react-native';
import { useJobProgress, useLibraryQuery } from '@sd/client'; import { useBridgeQuery, useJobProgress } from '@sd/client';
import JobGroup from '~/components/job/JobGroup'; import JobGroup from '~/components/job/JobGroup';
import { Modal, ModalRef } from '~/components/layout/Modal'; import { Modal, ModalRef } from '~/components/layout/Modal';
import { tw } from '~/lib/tailwind'; import { tw } from '~/lib/tailwind';
@ -11,10 +11,13 @@ import { tw } from '~/lib/tailwind';
// - Add clear all jobs button // - Add clear all jobs button
export const JobManagerModal = forwardRef<ModalRef, unknown>((_, ref) => { export const JobManagerModal = forwardRef<ModalRef, unknown>((_, ref) => {
const queryClient = useQueryClient(); const jobGroupsById = useBridgeQuery(['jobs.reports']);
const jobGroups = useLibraryQuery(['jobs.reports']); // TODO: Currently we're only clustering togheter all job reports from all libraries without any distinction.
const progress = useJobProgress(jobGroups.data); // TODO: We should probably cluster them by library in the job manager UI
const jobGroups = jobGroupsById.data ? Object.values(jobGroupsById.data).flat() : [];
const progress = useJobProgress(jobGroups);
// const clearAllJobs = useLibraryMutation(['jobs.clearAll'], { // const clearAllJobs = useLibraryMutation(['jobs.clearAll'], {
// onError: () => { // onError: () => {
@ -28,7 +31,7 @@ export const JobManagerModal = forwardRef<ModalRef, unknown>((_, ref) => {
return ( return (
<Modal ref={ref} snapPoints={['60']} title="Recent Jobs" showCloseButton> <Modal ref={ref} snapPoints={['60']} title="Recent Jobs" showCloseButton>
<FlatList <FlatList
data={jobGroups.data} data={jobGroups}
style={tw`flex-1`} style={tw`flex-1`}
keyExtractor={(i) => i.id} keyExtractor={(i) => i.id}
contentContainerStyle={tw`mt-4`} contentContainerStyle={tw`mt-4`}

View file

@ -1,10 +1,10 @@
module.exports = { module.exports = {
plugins: { plugins: {
tailwindcss: {}, 'tailwindcss': {},
autoprefixer: {}, 'autoprefixer': {},
"postcss-pseudo-companion-classes": { 'postcss-pseudo-companion-classes': {
prefix: "sb-pseudo--", prefix: 'sb-pseudo--',
restrictTo: [":hover", ":focus"] restrictTo: [':hover', ':focus']
}, }
}, }
}; };

View file

@ -1,6 +1,7 @@
use crate::{ use crate::{
invalidate_query, invalidate_query,
job::{job_without_data, Job, JobReport, JobStatus, Jobs}, job::{job_without_data, Job, JobReport, JobStatus, Jobs},
library::Library,
location::{find_location, LocationError}, location::{find_location, LocationError},
object::{ object::{
file_identifier::file_identifier_job::FileIdentifierJobInit, media::MediaProcessorJobInit, file_identifier::file_identifier_job::FileIdentifierJobInit, media::MediaProcessorJobInit,
@ -16,6 +17,7 @@ use std::{
}; };
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use futures_concurrency::future::TryJoin;
use prisma_client_rust::or; use prisma_client_rust::or;
use rspc::alpha::AlphaRouter; use rspc::alpha::AlphaRouter;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -78,86 +80,109 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
jobs: VecDeque<JobReport>, jobs: VecDeque<JobReport>,
} }
R.with2(library()) async fn group_jobs_by_library(
.query(|(node, library), _: ()| async move { library: &Library,
let mut groups: HashMap<String, JobGroup> = HashMap::new(); active_job_reports_by_id: &HashMap<Uuid, JobReport>,
) -> Result<Vec<JobGroup>, rspc::Error> {
let mut groups: HashMap<String, JobGroup> = HashMap::new();
let job_reports: Vec<JobReport> = library let job_reports: Vec<JobReport> = library
.db .db
.job() .job()
.find_many(vec![]) .find_many(vec![])
.order_by(job::date_created::order(SortOrder::Desc)) .order_by(job::date_created::order(SortOrder::Desc))
.take(100) .take(100)
.select(job_without_data::select()) .select(job_without_data::select())
.exec() .exec()
.await? .await?
.into_iter() .into_iter()
.flat_map(JobReport::try_from) .flat_map(JobReport::try_from)
.collect(); .collect();
let active_reports_by_id = node.jobs.get_active_reports_with_id().await; for job in job_reports {
// action name and group key are computed from the job data
let (action_name, group_key) = job.get_meta();
for job in job_reports { trace!(
// action name and group key are computed from the job data "job {:#?}, action_name {}, group_key {:?}",
let (action_name, group_key) = job.get_meta(); job,
action_name,
group_key
);
trace!( // if the job is running, use the in-memory report
"job {:#?}, action_name {}, group_key {:?}", let report = active_job_reports_by_id.get(&job.id).unwrap_or(&job);
job,
action_name,
group_key
);
// if the job is running, use the in-memory report // if we have a group key, handle grouping
let report = active_reports_by_id.get(&job.id).unwrap_or(&job); if let Some(group_key) = group_key {
match groups.entry(group_key) {
// if we have a group key, handle grouping // Create new job group with metadata
if let Some(group_key) = group_key { Entry::Vacant(entry) => {
match groups.entry(group_key) { entry.insert(JobGroup {
// Create new job group with metadata id: job.parent_id.unwrap_or(job.id),
Entry::Vacant(entry) => { action: Some(action_name.clone()),
entry.insert(JobGroup {
id: job.parent_id.unwrap_or(job.id),
action: Some(action_name.clone()),
status: job.status,
jobs: [report.clone()].into_iter().collect(),
created_at: job.created_at.unwrap_or(Utc::now()),
});
}
// Add to existing job group
Entry::Occupied(mut entry) => {
let group = entry.get_mut();
// protect paused status from being overwritten
if report.status != JobStatus::Paused {
group.status = report.status;
}
// if group.status.is_finished() && !report.status.is_finished() {
// }
group.jobs.push_front(report.clone());
}
}
} else {
// insert individual job as group
groups.insert(
job.id.to_string(),
JobGroup {
id: job.id,
action: None,
status: job.status, status: job.status,
jobs: [report.clone()].into_iter().collect(), jobs: [report.clone()].into_iter().collect(),
created_at: job.created_at.unwrap_or(Utc::now()), created_at: job.created_at.unwrap_or(Utc::now()),
}, });
); }
// Add to existing job group
Entry::Occupied(mut entry) => {
let group = entry.get_mut();
// protect paused status from being overwritten
if report.status != JobStatus::Paused {
group.status = report.status;
}
// if group.status.is_finished() && !report.status.is_finished() {
// }
group.jobs.push_front(report.clone());
}
} }
} else {
// insert individual job as group
groups.insert(
job.id.to_string(),
JobGroup {
id: job.id,
action: None,
status: job.status,
jobs: [report.clone()].into_iter().collect(),
created_at: job.created_at.unwrap_or(Utc::now()),
},
);
} }
}
let mut groups_vec = groups.into_values().collect::<Vec<_>>(); let mut groups_vec = groups.into_values().collect::<Vec<_>>();
groups_vec.sort_by(|a, b| b.created_at.cmp(&a.created_at)); groups_vec.sort_by(|a, b| b.created_at.cmp(&a.created_at));
Ok(groups_vec) Ok(groups_vec)
}) }
R.query(|node, _: ()| async move {
// WARN: We really need the borrow in this line, this way each async move in the map below
// received a copy of the reference to the active job reports,
// I feel like I'm conquering the borrow checker
let active_job_reports_by_id = &node.jobs.get_active_reports_with_id().await;
node.libraries
.get_all()
.await
.into_iter()
.map(|library| async move {
group_jobs_by_library(&library, active_job_reports_by_id)
.await
.map(|groups| (library.id, groups))
})
.collect::<Vec<_>>()
.try_join()
.await
.map(|groups_by_library_id| {
groups_by_library_id.into_iter().collect::<HashMap<_, _>>()
})
})
}) })
.procedure("isActive", { .procedure("isActive", {
R.with2(library()) R.with2(library())

View file

@ -164,7 +164,11 @@ pub fn router(node: Arc<Node>) -> Router<()> {
name: path, name: path,
ext: maybe_missing(file_path.extension, "extension").map_err(not_found)?, ext: maybe_missing(file_path.extension, "extension").map_err(not_found)?,
file_path_pub_id: Uuid::from_slice(&file_path.pub_id).map_err(internal_server_error)?, file_path_pub_id: Uuid::from_slice(&file_path.pub_id).map_err(internal_server_error)?,
serve_from: (identity == library.identity.to_remote_identity()).then_some(ServeFrom::Local).unwrap_or_else(|| ServeFrom::Remote(identity)), serve_from: if identity == library.identity.to_remote_identity() {
ServeFrom::Local
} else {
ServeFrom::Remote(identity)
},
}; };
state state
@ -202,8 +206,8 @@ pub fn router(node: Arc<Node>) -> Router<()> {
} }
// TODO: Support `Range` requests and `ETag` headers // TODO: Support `Range` requests and `ETag` headers
#[allow(clippy::unwrap_used)]
match state.node.nlm.state().await.get(&library_id).unwrap().instances.get(&identity).unwrap().clone() { match *state.node.nlm.state().await.get(&library_id).unwrap().instances.get(&identity).unwrap() {
InstanceState::Discovered(_) | InstanceState::Unavailable => Ok(not_found(())), InstanceState::Discovered(_) | InstanceState::Unavailable => Ok(not_found(())),
InstanceState::Connected(peer_id) => { InstanceState::Connected(peer_id) => {
let (tx, mut rx) = tokio::sync::mpsc::channel::<io::Result<Bytes>>(150); let (tx, mut rx) = tokio::sync::mpsc::channel::<io::Result<Bytes>>(150);
@ -620,6 +624,7 @@ impl AsyncWrite for MpscToAsyncWrite {
cx: &mut Context<'_>, cx: &mut Context<'_>,
buf: &[u8], buf: &[u8],
) -> Poll<Result<usize, io::Error>> { ) -> Poll<Result<usize, io::Error>> {
#[allow(clippy::unwrap_used)]
match self.0.poll_reserve(cx) { match self.0.poll_reserve(cx) {
Poll::Ready(Ok(())) => { Poll::Ready(Ok(())) => {
self.0.send_item(Ok(Bytes::from(buf.to_vec()))).unwrap(); self.0.send_item(Ok(Bytes::from(buf.to_vec()))).unwrap();

View file

@ -628,7 +628,7 @@ impl P2PManager {
.write_all( .write_all(
&Header::File { &Header::File {
library_id: library.id, library_id: library.id,
file_path_id: file_path_id.clone(), file_path_id,
range: range.clone(), range: range.clone(),
} }
.to_bytes(), .to_bytes(),

View file

@ -209,7 +209,7 @@ export const ParentFolderActions = new ConditionalItem({
await generateThumbnails.mutateAsync({ await generateThumbnails.mutateAsync({
id: parent.location.id, id: parent.location.id,
path: selectedFilePaths[0]?.materialized_path ?? '/', path: selectedFilePaths[0]?.materialized_path ?? '/',
regenerate: true, regenerate: true
}); });
} catch (error) { } catch (error) {
toast.error({ toast.error({

View file

@ -130,7 +130,7 @@ export default (props: PropsWithChildren) => {
await generateThumbsForLocation.mutateAsync({ await generateThumbsForLocation.mutateAsync({
id: parent.location.id, id: parent.location.id,
path: currentPath ?? '/', path: currentPath ?? '/',
regenerate: true, regenerate: true
}); });
} catch (error) { } catch (error) {
toast.error({ toast.error({

View file

@ -46,7 +46,7 @@ export default function FeedbackDialog(props: UseDialogProps) {
ctaLabel="Submit" ctaLabel="Submit"
closeLabel="Cancel" closeLabel="Cancel"
buttonsSideContent={ buttonsSideContent={
<div className="flex items-center justify-center w-full gap-1"> <div className="flex w-full items-center justify-center gap-1">
{EMOJIS.map((emoji, i) => ( {EMOJIS.map((emoji, i) => (
<div <div
onClick={() => emojiSelectHandler(i)} onClick={() => emojiSelectHandler(i)}

View file

@ -1,7 +1,7 @@
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { Check, Trash, X } from 'phosphor-react'; import { Check, Trash, X } from 'phosphor-react';
import { useState } from 'react'; import { useState } from 'react';
import { useJobProgress, useLibraryMutation, useLibraryQuery } from '@sd/client'; import { useBridgeQuery, useJobProgress, useLibraryMutation } from '@sd/client';
import { Button, PopoverClose, Tooltip, toast } from '@sd/ui'; import { Button, PopoverClose, Tooltip, toast } from '@sd/ui';
import IsRunningJob from './IsRunningJob'; import IsRunningJob from './IsRunningJob';
import JobGroup from './JobGroup'; import JobGroup from './JobGroup';
@ -10,9 +10,13 @@ export function JobManager() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [toggleConfirmation, setToggleConfirmation] = useState(false); const [toggleConfirmation, setToggleConfirmation] = useState(false);
const jobGroups = useLibraryQuery(['jobs.reports']); const jobGroupsById = useBridgeQuery(['jobs.reports']);
const progress = useJobProgress(jobGroups.data); // TODO: Currently we're only clustering togheter all job reports from all libraries without any distinction.
// TODO: We should probably cluster them by library in the job manager UI
const jobGroups = jobGroupsById.data ? Object.values(jobGroupsById.data).flat() : [];
const progress = useJobProgress(jobGroups);
const clearAllJobs = useLibraryMutation(['jobs.clearAll'], { const clearAllJobs = useLibraryMutation(['jobs.clearAll'], {
onError: () => { onError: () => {
@ -76,16 +80,15 @@ export function JobManager() {
</div> </div>
<div className="custom-scroll job-manager-scroll h-full overflow-x-hidden"> <div className="custom-scroll job-manager-scroll h-full overflow-x-hidden">
<div className="h-full border-r border-app-line/50"> <div className="h-full border-r border-app-line/50">
{jobGroups.data && {jobGroups.length === 0 ? (
(jobGroups.data.length === 0 ? ( <div className="flex h-32 items-center justify-center text-sidebar-inkDull">
<div className="flex h-32 items-center justify-center text-sidebar-inkDull"> No jobs.
No jobs. </div>
</div> ) : (
) : ( jobGroups.map((group) => (
jobGroups.data.map((group) => ( <JobGroup key={group.id} group={group} progress={progress} />
<JobGroup key={group.id} group={group} progress={progress} /> ))
)) )}
))}
</div> </div>
</div> </div>
</div> </div>

View file

@ -216,7 +216,7 @@ export const AddLocationDialog = ({
: '' : ''
} }
> >
<ErrorMessage name={REMOTE_ERROR_FORM_FIELD} variant="large" className="mt-2 mb-4" /> <ErrorMessage name={REMOTE_ERROR_FORM_FIELD} variant="large" className="mb-4 mt-2" />
<InputField <InputField
size="md" size="md"

View file

@ -12,7 +12,7 @@ export type Procedures = {
{ key: "files.getPath", input: LibraryArgs<number>, result: string | null } | { key: "files.getPath", input: LibraryArgs<number>, result: string | null } |
{ key: "invalidation.test-invalidate", input: never, result: number } | { key: "invalidation.test-invalidate", input: never, result: number } |
{ key: "jobs.isActive", input: LibraryArgs<null>, result: boolean } | { key: "jobs.isActive", input: LibraryArgs<null>, result: boolean } |
{ key: "jobs.reports", input: LibraryArgs<null>, result: JobGroup[] } | { key: "jobs.reports", input: never, result: { [key: string]: JobGroup[] } } |
{ key: "library.list", input: never, result: LibraryConfigWrapped[] } | { key: "library.list", input: never, result: LibraryConfigWrapped[] } |
{ key: "library.statistics", input: LibraryArgs<null>, result: Statistics } | { key: "library.statistics", input: LibraryArgs<null>, result: Statistics } |
{ key: "locations.get", input: LibraryArgs<number>, result: Location | null } | { key: "locations.get", input: LibraryArgs<number>, result: Location | null } |

View file

@ -30,7 +30,7 @@ export const AllVariants = () => {
'subtle' 'subtle'
]; ];
return ( return (
<div className="w-full h-screen p-10 bg-app"> <div className="h-screen w-full bg-app p-10">
<h1 className="text-[20px] font-bold text-white">Buttons</h1> <h1 className="text-[20px] font-bold text-white">Buttons</h1>
<div className="mb-6 ml-[90px] mt-5 flex flex-col gap-8 text-sm"> <div className="mb-6 ml-[90px] mt-5 flex flex-col gap-8 text-sm">
<div className="ml-[100px] grid w-full max-w-[850px] grid-cols-9 items-center gap-6"> <div className="ml-[100px] grid w-full max-w-[850px] grid-cols-9 items-center gap-6">