mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-04 13:23:28 +00:00
[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:
parent
5860016789
commit
f8033d1842
|
@ -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`}
|
||||||
|
|
|
@ -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']
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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)}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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 } |
|
||||||
|
|
|
@ -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">
|
||||||
|
|
Loading…
Reference in a new issue