diff --git a/.vscode/settings.json b/.vscode/settings.json index 421272d60..cedc4f182 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,6 +17,7 @@ "proptype", "quicktime", "repr", + "rescan", "Roadmap", "subpackage", "svgr", diff --git a/apps/server/package.json b/apps/server/package.json index 56e19508c..75d37d974 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -2,5 +2,8 @@ "name": "@sd/server", "version": "0.0.0", "main": "index.js", - "license": "GPL-3.0-only" + "license": "GPL-3.0-only", + "scripts": { + "dev": "cargo watch -x 'run -p server'" + } } diff --git a/core/bindings/ClientCommand.ts b/core/bindings/ClientCommand.ts index 515e64d80..2677dd55f 100644 --- a/core/bindings/ClientCommand.ts +++ b/core/bindings/ClientCommand.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type ClientCommand = { key: "FileReadMetaData", params: { id: number, } } | { key: "FileSetNote", params: { id: number, note: string | null, } } | { key: "FileDelete", params: { id: number, } } | { key: "LibDelete", params: { id: number, } } | { key: "TagCreate", params: { name: string, color: string, } } | { key: "TagUpdate", params: { name: string, color: string, } } | { key: "TagAssign", params: { file_id: number, tag_id: number, } } | { key: "TagDelete", params: { id: number, } } | { key: "LocCreate", params: { path: string, } } | { key: "LocUpdate", params: { id: number, name: string | null, } } | { key: "LocDelete", params: { id: number, } } | { key: "SysVolumeUnmount", params: { id: number, } } | { key: "GenerateThumbsForLocation", params: { id: number, path: string, } } | { key: "IdentifyUniqueFiles", params: { id: number, path: string, } }; \ No newline at end of file +export type ClientCommand = { key: "FileReadMetaData", params: { id: number, } } | { key: "FileSetNote", params: { id: number, note: string | null, } } | { key: "FileDelete", params: { id: number, } } | { key: "LibDelete", params: { id: number, } } | { key: "TagCreate", params: { name: string, color: string, } } | { key: "TagUpdate", params: { name: string, color: string, } } | { key: "TagAssign", params: { file_id: number, tag_id: number, } } | { key: "TagDelete", params: { id: number, } } | { key: "LocCreate", params: { path: string, } } | { key: "LocUpdate", params: { id: number, name: string | null, } } | { key: "LocDelete", params: { id: number, } } | { key: "LocRescan", params: { id: number, } } | { key: "SysVolumeUnmount", params: { id: number, } } | { key: "GenerateThumbsForLocation", params: { id: number, path: string, } } | { key: "IdentifyUniqueFiles", params: { id: number, path: string, } }; \ No newline at end of file diff --git a/core/bindings/JobReport.ts b/core/bindings/JobReport.ts index bb1142c93..bd25c8d21 100644 --- a/core/bindings/JobReport.ts +++ b/core/bindings/JobReport.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { JobStatus } from "./JobStatus"; -export interface JobReport { id: string, name: string, date_created: string, date_modified: string, status: JobStatus, task_count: number, completed_task_count: number, message: string, seconds_elapsed: string, } \ No newline at end of file +export interface JobReport { id: string, name: string, data: string | null, date_created: string, date_modified: string, status: JobStatus, task_count: number, completed_task_count: number, message: string, seconds_elapsed: string, } \ No newline at end of file diff --git a/core/bindings/LibraryNode.ts b/core/bindings/LibraryNode.ts index a3994802e..f5c82dd96 100644 --- a/core/bindings/LibraryNode.ts +++ b/core/bindings/LibraryNode.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { Platform } from "./Platform"; -export interface LibraryNode { uuid: string, name: string, platform: Platform, tcp_address: string, last_seen: string, last_synchronized: string, } \ No newline at end of file +export interface LibraryNode { uuid: string, name: string, platform: Platform, last_seen: string, } \ No newline at end of file diff --git a/core/bindings/LocationResource.ts b/core/bindings/LocationResource.ts index d64b3d097..c9fbfa335 100644 --- a/core/bindings/LocationResource.ts +++ b/core/bindings/LocationResource.ts @@ -1,3 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { LibraryNode } from "./LibraryNode"; -export interface LocationResource { id: number, name: string | null, path: string | null, total_capacity: number | null, available_capacity: number | null, is_removable: boolean | null, is_online: boolean, date_created: string, } \ No newline at end of file +export interface LocationResource { id: number, name: string | null, path: string | null, total_capacity: number | null, available_capacity: number | null, is_removable: boolean | null, node: LibraryNode | null, is_online: boolean, date_created: string, } \ No newline at end of file diff --git a/core/prisma/migrations/20220624132352_location_node_relation/migration.sql b/core/prisma/migrations/20220624132352_location_node_relation/migration.sql new file mode 100644 index 000000000..008fc8ee7 --- /dev/null +++ b/core/prisma/migrations/20220624132352_location_node_relation/migration.sql @@ -0,0 +1,48 @@ +-- AlterTable +ALTER TABLE "jobs" ADD COLUMN "data" TEXT; + +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_locations" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "pub_id" TEXT NOT NULL, + "node_id" INTEGER, + "name" TEXT, + "local_path" TEXT, + "total_capacity" INTEGER, + "available_capacity" INTEGER, + "filesystem" TEXT, + "disk_type" INTEGER, + "is_removable" BOOLEAN, + "is_online" BOOLEAN NOT NULL DEFAULT true, + "date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "locations_node_id_fkey" FOREIGN KEY ("node_id") REFERENCES "nodes" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); +INSERT INTO "new_locations" ("available_capacity", "date_created", "disk_type", "filesystem", "id", "is_online", "is_removable", "local_path", "name", "node_id", "pub_id", "total_capacity") SELECT "available_capacity", "date_created", "disk_type", "filesystem", "id", "is_online", "is_removable", "local_path", "name", "node_id", "pub_id", "total_capacity" FROM "locations"; +DROP TABLE "locations"; +ALTER TABLE "new_locations" RENAME TO "locations"; +CREATE UNIQUE INDEX "locations_pub_id_key" ON "locations"("pub_id"); +CREATE TABLE "new_file_paths" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "is_dir" BOOLEAN NOT NULL DEFAULT false, + "location_id" INTEGER, + "materialized_path" TEXT NOT NULL, + "name" TEXT NOT NULL, + "extension" TEXT, + "file_id" INTEGER, + "parent_id" INTEGER, + "key_id" INTEGER, + "date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "date_modified" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "date_indexed" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "file_paths_location_id_fkey" FOREIGN KEY ("location_id") REFERENCES "locations" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "file_paths_file_id_fkey" FOREIGN KEY ("file_id") REFERENCES "files" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "file_paths_key_id_fkey" FOREIGN KEY ("key_id") REFERENCES "keys" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); +INSERT INTO "new_file_paths" ("date_created", "date_indexed", "date_modified", "extension", "file_id", "id", "is_dir", "key_id", "location_id", "materialized_path", "name", "parent_id") SELECT "date_created", "date_indexed", "date_modified", "extension", "file_id", "id", "is_dir", "key_id", "location_id", "materialized_path", "name", "parent_id" FROM "file_paths"; +DROP TABLE "file_paths"; +ALTER TABLE "new_file_paths" RENAME TO "file_paths"; +CREATE INDEX "file_paths_location_id_idx" ON "file_paths"("location_id"); +CREATE UNIQUE INDEX "file_paths_location_id_materialized_path_name_extension_key" ON "file_paths"("location_id", "materialized_path", "name", "extension"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/core/prisma/schema.prisma b/core/prisma/schema.prisma index d15782e34..e8f911004 100644 --- a/core/prisma/schema.prisma +++ b/core/prisma/schema.prisma @@ -74,6 +74,7 @@ model Node { sync_events SyncEvent[] jobs Job[] + Location Location[] @@map("nodes") } @@ -107,14 +108,14 @@ model Location { is_online Boolean @default(true) date_created DateTime @default(now()) + node Node? @relation(fields: [node_id], references: [id]) file_paths FilePath[] @@map("locations") } model File { id Int @id @default(autoincrement()) - // content addressable storage id - sha256 - // this does not need to be unique, as incoming replicas will always ignore if at least one exists + // content addressable storage id - sha256 sampled checksum cas_id String @unique // full byte contents digested into sha256 checksum integrity_checksum String? @unique @@ -157,7 +158,7 @@ model FilePath { id Int @id @default(autoincrement()) is_dir Boolean @default(false) // location that owns this path - location_id Int + location_id Int? // a path generated from local file_path ids eg: "34/45/67/890" materialized_path String // the name and extension @@ -175,14 +176,17 @@ model FilePath { date_modified DateTime @default(now()) date_indexed DateTime @default(now()) - file File? @relation(fields: [file_id], references: [id], onDelete: Cascade, onUpdate: Cascade) - location Location? @relation(fields: [location_id], references: [id], onDelete: Cascade, onUpdate: Cascade) - parent FilePath? @relation("directory_file_paths", fields: [parent_id], references: [id]) - children FilePath[] @relation("directory_file_paths") + file File? @relation(fields: [file_id], references: [id], onDelete: Cascade, onUpdate: Cascade) + location Location? @relation(fields: [location_id], references: [id], onDelete: Cascade, onUpdate: Cascade) + + // NOTE: this self relation for the file tree was causing SQLite to go to forever bed, disabling until workaround + // parent FilePath? @relation("directory_file_paths", fields: [parent_id], references: [id], onDelete: NoAction, onUpdate: NoAction) + // children FilePath[] @relation("directory_file_paths") key Key? @relation(fields: [key_id], references: [id]) @@unique([location_id, materialized_path, name, extension]) + @@index([location_id]) @@map("file_paths") } @@ -282,11 +286,12 @@ model LabelOnFile { } model Job { - id String @id + id String @id name String node_id Int action Int - status Int @default(0) + status Int @default(0) + data String? task_count Int @default(1) completed_task_count Int @default(0) diff --git a/core/src/encode/thumb.rs b/core/src/encode/thumb.rs index 73b3249f7..bb148d46b 100644 --- a/core/src/encode/thumb.rs +++ b/core/src/encode/thumb.rs @@ -151,7 +151,7 @@ pub async fn get_images( path: &str, ) -> Result, std::io::Error> { let mut params = vec![ - file_path::location_id::equals(location_id), + file_path::location_id::equals(Some(location_id)), file_path::extension::in_vec(vec![ "png".to_string(), "jpeg".to_string(), diff --git a/core/src/file/cas/identifier.rs b/core/src/file/cas/identifier.rs index 7616c92a6..94e5c5aaf 100644 --- a/core/src/file/cas/identifier.rs +++ b/core/src/file/cas/identifier.rs @@ -159,7 +159,12 @@ impl Job for FileIdentifierJob { } // handle loop end - let last_row = file_paths.last().unwrap(); + let last_row = match file_paths.last() { + Some(l) => l, + None => { + break; + } + }; cursor = last_row.id; completed += 1; diff --git a/core/src/file/explorer/open.rs b/core/src/file/explorer/open.rs index ddf110ac9..bedfba5a0 100644 --- a/core/src/file/explorer/open.rs +++ b/core/src/file/explorer/open.rs @@ -22,7 +22,7 @@ pub async fn open_dir( let directory = db .file_path() .find_first(vec![ - file_path::location_id::equals(location.id), + file_path::location_id::equals(Some(location.id)), file_path::materialized_path::equals(path.into()), file_path::is_dir::equals(true), ]) @@ -35,7 +35,7 @@ pub async fn open_dir( let mut file_paths: Vec = db .file_path() .find_many(vec![ - file_path::location_id::equals(location.id), + file_path::location_id::equals(Some(location.id)), file_path::parent_id::equals(Some(directory.id)), ]) .with(file_path::file::fetch()) diff --git a/core/src/file/mod.rs b/core/src/file/mod.rs index 7d1232d35..bc632ecec 100644 --- a/core/src/file/mod.rs +++ b/core/src/file/mod.rs @@ -110,7 +110,7 @@ impl Into for file_path::Data { materialized_path: self.materialized_path, file_id: self.file_id, parent_id: self.parent_id, - location_id: self.location_id, + location_id: self.location_id.unwrap_or(0), date_indexed: self.date_indexed.into(), name: self.name, extension: self.extension, diff --git a/core/src/job/jobs.rs b/core/src/job/jobs.rs index befc88042..efacd6cb8 100644 --- a/core/src/job/jobs.rs +++ b/core/src/job/jobs.rs @@ -18,7 +18,8 @@ use std::{ use tokio::sync::Mutex; use ts_rs::TS; -const MAX_WORKERS: usize = 4; +// db is single threaded, nerd +const MAX_WORKERS: usize = 1; #[async_trait::async_trait] pub trait Job: Send + Sync + Debug { @@ -40,6 +41,7 @@ impl Jobs { running_workers: HashMap::new(), } } + pub async fn ingest(&mut self, ctx: &CoreContext, job: Box) { // create worker to process job if self.running_workers.len() < MAX_WORKERS { @@ -57,6 +59,7 @@ impl Jobs { self.job_queue.push_back(job); } } + pub fn ingest_queue(&mut self, _ctx: &CoreContext, job: Box) { self.job_queue.push_back(job); } @@ -69,6 +72,7 @@ impl Jobs { self.ingest(ctx, job).await; } } + pub async fn get_running(&self) -> Vec { let mut ret = vec![]; @@ -78,6 +82,19 @@ impl Jobs { } ret } + + pub async fn queue_pending_job(ctx: &CoreContext) -> Result<(), JobError> { + let db = &ctx.database; + + let next_job = db + .job() + .find_first(vec![job::status::equals(JobStatus::Queued.int_value())]) + .exec() + .await?; + + Ok(()) + } + pub async fn get_history(ctx: &CoreContext) -> Result, JobError> { let db = &ctx.database; let jobs = db @@ -103,6 +120,7 @@ pub enum JobReportUpdate { pub struct JobReport { pub id: String, pub name: String, + pub data: Option, // client_id: i32, #[ts(type = "string")] pub date_created: chrono::DateTime, @@ -131,6 +149,7 @@ impl Into for job::Data { completed_task_count: self.completed_task_count, date_created: self.date_created.into(), date_modified: self.date_modified.into(), + data: self.data, message: String::new(), seconds_elapsed: self.seconds_elapsed, } @@ -147,6 +166,7 @@ impl JobReport { date_modified: chrono::Utc::now(), status: JobStatus::Queued, task_count: 0, + data: None, completed_task_count: 0, message: String::new(), seconds_elapsed: 0, @@ -154,6 +174,13 @@ impl JobReport { } pub async fn create(&self, ctx: &CoreContext) -> Result<(), JobError> { let config = get_nodestate(); + + let mut params = Vec::new(); + + if let Some(_) = &self.data { + params.push(job::data::set(self.data.clone())) + } + ctx.database .job() .create( @@ -161,7 +188,7 @@ impl JobReport { job::name::set(self.name.clone()), job::action::set(1), job::nodes::link(node::id::equals(config.node_id)), - vec![], + params, ) .exec() .await?; diff --git a/core/src/job/worker.rs b/core/src/job/worker.rs index a58e1c987..6022e603e 100644 --- a/core/src/job/worker.rs +++ b/core/src/job/worker.rs @@ -36,6 +36,10 @@ impl WorkerContext { .send(WorkerEvent::Progressed(updates)) .unwrap_or(()); } + // save the job data to + // pub fn save_data () { + + // } } // a worker is a dedicated thread that runs a single job diff --git a/core/src/lib.rs b/core/src/lib.rs index b9ecce4a8..bb096a666 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -258,13 +258,11 @@ impl Node { CoreResponse::Success(()) } ClientCommand::LocDelete { id } => { - ctx.database - .location() - .find_unique(location::id::equals(id)) - .delete() - .exec() - .await?; - + sys::delete_location(&ctx, id).await?; + CoreResponse::Success(()) + } + ClientCommand::LocRescan { id } => { + sys::scan_location(&ctx, id, String::new()); CoreResponse::Success(()) } // CRUD for files @@ -374,6 +372,7 @@ pub enum ClientCommand { LocCreate { path: String }, LocUpdate { id: i32, name: Option }, LocDelete { id: i32 }, + LocRescan { id: i32 }, // System SysVolumeUnmount { id: i32 }, GenerateThumbsForLocation { id: i32, path: String }, diff --git a/core/src/node/mod.rs b/core/src/node/mod.rs index 88ead7825..52bed0ef9 100644 --- a/core/src/node/mod.rs +++ b/core/src/node/mod.rs @@ -19,11 +19,18 @@ pub struct LibraryNode { pub uuid: String, pub name: String, pub platform: Platform, - pub tcp_address: String, - #[ts(type = "string")] pub last_seen: DateTime, - #[ts(type = "string")] - pub last_synchronized: DateTime, +} + +impl Into for node::Data { + fn into(self) -> LibraryNode { + LibraryNode { + uuid: self.pub_id, + name: self.name, + platform: IntEnum::from_int(self.platform).unwrap(), + last_seen: self.last_seen.into(), + } + } } #[repr(i32)] diff --git a/core/src/sys/locations.rs b/core/src/sys/locations.rs index cf2b52578..0b4dafac3 100644 --- a/core/src/sys/locations.rs +++ b/core/src/sys/locations.rs @@ -1,10 +1,11 @@ use crate::{ encode::ThumbnailJob, file::{cas::FileIdentifierJob, indexer::IndexerJob}, - node::get_nodestate, - prisma::location, + node::{get_nodestate, LibraryNode}, + prisma::{file_path, location}, ClientQuery, CoreContext, CoreEvent, }; +use prisma_client_rust::{raw, PrismaValue}; use serde::{Deserialize, Serialize}; use std::{fs, io, io::Write, path::Path}; use thiserror::Error; @@ -21,13 +22,14 @@ pub struct LocationResource { pub total_capacity: Option, pub available_capacity: Option, pub is_removable: Option, + pub node: Option, pub is_online: bool, #[ts(type = "string")] pub date_created: chrono::DateTime, } impl Into for location::Data { - fn into(self) -> LocationResource { + fn into(mut self) -> LocationResource { LocationResource { id: self.id, name: self.name, @@ -35,6 +37,7 @@ impl Into for location::Data { total_capacity: self.total_capacity, available_capacity: self.available_capacity, is_removable: self.is_removable, + node: self.node.take().unwrap_or(None).map(|node| (*node).into()), is_online: self.is_online, date_created: self.date_created.into(), } @@ -81,26 +84,24 @@ pub async fn get_location( Ok(location.into()) } +pub fn scan_location(ctx: &CoreContext, location_id: i32, path: String) { + ctx.spawn_job(Box::new(IndexerJob { path: path.clone() })); + ctx.queue_job(Box::new(FileIdentifierJob { location_id, path })); + // TODO: make a way to stop jobs so this can be canceled without rebooting app + // ctx.queue_job(Box::new(ThumbnailJob { + // location_id, + // path: "".to_string(), + // background: false, + // })); +} + pub async fn new_location_and_scan( ctx: &CoreContext, path: &str, ) -> Result { let location = create_location(&ctx, path).await?; - ctx.spawn_job(Box::new(IndexerJob { - path: path.to_string(), - })); - - ctx.queue_job(Box::new(FileIdentifierJob { - location_id: location.id, - path: path.to_string(), - })); - - ctx.queue_job(Box::new(ThumbnailJob { - location_id: location.id, - path: "".to_string(), - background: false, - })); + scan_location(&ctx, location.id, path.to_string()); Ok(location) } @@ -108,7 +109,12 @@ pub async fn new_location_and_scan( pub async fn get_locations(ctx: &CoreContext) -> Result, SysError> { let db = &ctx.database; - let locations = db.location().find_many(vec![]).exec().await?; + let locations = db + .location() + .find_many(vec![]) + .with(location::node::fetch()) + .exec() + .await?; // turn locations into LocationResource let locations: Vec = locations @@ -175,6 +181,7 @@ pub async fn create_location(ctx: &CoreContext, path: &str) -> Result Result Result<(), SysError> { + let db = &ctx.database; + + db.file_path() + .find_many(vec![file_path::location_id::equals(Some(location_id))]) + .delete() + .exec() + .await?; + + db.location() + .find_unique(location::id::equals(location_id)) + .delete() + .exec() + .await?; + + ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::SysGetLocations)) + .await; + + println!("Location {} deleted", location_id); + + Ok(()) +} + #[derive(Error, Debug)] pub enum LocationError { #[error("Failed to create location (uuid {uuid:?})")] diff --git a/packages/interface/src/AppLayout.tsx b/packages/interface/src/AppLayout.tsx index 041fec5fc..7b1b2dfc5 100644 --- a/packages/interface/src/AppLayout.tsx +++ b/packages/interface/src/AppLayout.tsx @@ -19,7 +19,7 @@ export function AppLayout() { return false; }} className={clsx( - 'flex flex-row h-screen overflow-hidden text-gray-900 select-none dark:text-white', + 'flex flex-row h-screen overflow-hidden text-gray-900 select-none dark:text-white cursor-default', isWindowRounded && 'rounded-xl', hasWindowBorder && 'border border-gray-200 dark:border-gray-500' )} diff --git a/packages/interface/src/components/device/Device.tsx b/packages/interface/src/components/device/Device.tsx index d9fb49107..6f91b2e26 100644 --- a/packages/interface/src/components/device/Device.tsx +++ b/packages/interface/src/components/device/Device.tsx @@ -6,6 +6,7 @@ import React, { useState } from 'react'; import { Rings } from 'react-loading-icons'; import FileItem from '../file/FileItem'; +import Loader from '../primitive/Loader'; import ProgressBar from '../primitive/ProgressBar'; export interface DeviceProps { @@ -45,13 +46,7 @@ export function Device(props: DeviceProps) {
{props.runningJob && (
- +
{props.runningJob.task}... diff --git a/packages/interface/src/components/file/Inspector.tsx b/packages/interface/src/components/file/Inspector.tsx index c139ac596..aa527bb1a 100644 --- a/packages/interface/src/components/file/Inspector.tsx +++ b/packages/interface/src/components/file/Inspector.tsx @@ -35,20 +35,20 @@ export const Inspector = (props: { location?: LocationResource; selectedFile?: FilePath; }) => { - const file_path = props.selectedFile; - let full_path = `${props.location?.path}/${file_path?.materialized_path}`; + const file_path = props.selectedFile, + full_path = `${props.location?.path}/${file_path?.materialized_path}`, + file_id = props.selectedFile?.file?.id || -1; - // notes are stored in global state by their file id + // notes are cached in a store by their file id // this is so we can ensure every note has been sent to Rust even // when quickly navigating files, which cancels update function const { notes, setNote, unCacheNote } = useInspectorState(); - const file_id = props.selectedFile?.file?.id || -1; - // show cached note over server note + // show cached note over server note, important to check for undefined not falsey const note = notes[file_id] === undefined ? props.selectedFile?.file?.note || null : notes[file_id]; - // when input is updated + // when input is updated, cache note function handleNoteUpdate(e: React.ChangeEvent) { if (e.target.value !== note) { setNote(file_id, e.target.value); @@ -63,95 +63,95 @@ export const Inspector = (props: { }, [note]); return ( - -
- {!!file_path && ( -
-
- -
-

{file_path?.name}

-
- - - -
- {file_path?.file?.cas_id && ( - - )} - - - - +
+ {!!file_path && ( +
+
+ - - - {!file_path?.is_dir && ( - <> - -
- {file_path?.extension && ( - - {file_path?.extension} - - )} -

- {file_path?.extension - ? //@ts-ignore - types[file_path.extension.toUpperCase()]?.descriptions.join(' / ') - : 'Unknown'} -

-
- {file_path.file && ( - <> - - - } - /> - +
+

{file_path?.name}

+
+ + + +
+ {file_path?.file?.cas_id && ( + + )} + + + + + + + {!file_path?.is_dir && ( + <> + +
+ {file_path?.extension && ( + + {file_path?.extension} + )} - - )} - {/*
+

+ {file_path?.extension + ? //@ts-ignore + types[file_path.extension.toUpperCase()]?.descriptions.join(' / ') + : 'Unknown'} +

+
+ {file_path.file && ( + <> + + + } + /> + + )} + + )} + {/*
*/} - {/* + {/* */} -
- )} -
- +
+ )} +
+ // ); }; diff --git a/packages/interface/src/components/file/Sidebar.tsx b/packages/interface/src/components/file/Sidebar.tsx index 4ec803b6a..6b605f9c9 100644 --- a/packages/interface/src/components/file/Sidebar.tsx +++ b/packages/interface/src/components/file/Sidebar.tsx @@ -93,7 +93,7 @@ export const Sidebar: React.FC = (props) => { return (
void; title?: string; description?: string; - children: ReactNode; + children?: ReactNode; + loading?: boolean; } export default function Dialog(props: DialogProps) { @@ -26,12 +31,21 @@ export default function Dialog(props: DialogProps) { {props.children}
+ {props.loading && } +
- -
diff --git a/packages/interface/src/components/layout/TopBar.tsx b/packages/interface/src/components/layout/TopBar.tsx index e0eed7088..a451dc7f1 100644 --- a/packages/interface/src/components/layout/TopBar.tsx +++ b/packages/interface/src/components/layout/TopBar.tsx @@ -104,7 +104,7 @@ export const TopBar: React.FC = (props) => { { - generateThumbsForLocation({ id: locationId, path: '' }); + // generateThumbsForLocation({ id: locationId, path: '' }); }} />
diff --git a/packages/interface/src/components/location/LocationListItem.tsx b/packages/interface/src/components/location/LocationListItem.tsx new file mode 100644 index 000000000..7b20e759c --- /dev/null +++ b/packages/interface/src/components/location/LocationListItem.tsx @@ -0,0 +1,86 @@ +import { DotsVerticalIcon, RefreshIcon } from '@heroicons/react/outline'; +import { CogIcon, TrashIcon } from '@heroicons/react/solid'; +import { command, useBridgeCommand } from '@sd/client'; +import { LocationResource } from '@sd/core'; +import { Button } from '@sd/ui'; +import clsx from 'clsx'; +import React, { useState } from 'react'; + +import { Folder } from '../icons/Folder'; +import Dialog from '../layout/Dialog'; + +interface LocationListItemProps { + location: LocationResource; +} + +export default function LocationListItem({ location }: LocationListItemProps) { + const [hide, setHide] = useState(false); + + const { mutate: locRescan } = useBridgeCommand('LocRescan'); + + const { mutate: deleteLoc, isLoading: locDeletePending } = useBridgeCommand('LocDelete', { + onSuccess: () => { + setHide(true); + } + }); + + if (hide) return <>; + + return ( +
+ + +
+

{location.name}

+

+ {location.node?.name} + {location.path} +

+
+
+
+ + { + deleteLoc({ id: location.id }); + }} + loading={locDeletePending} + ctaDanger + ctaLabel="Delete" + trigger={ + + } + /> + + {/* */} +
+
+ ); +} diff --git a/packages/interface/src/components/primitive/Loader.tsx b/packages/interface/src/components/primitive/Loader.tsx new file mode 100644 index 000000000..f68a55808 --- /dev/null +++ b/packages/interface/src/components/primitive/Loader.tsx @@ -0,0 +1,15 @@ +import clsx from 'clsx'; +import React from 'react'; +import { Puff } from 'react-loading-icons'; + +export default function Loader(props: { className?: string }) { + return ( + + ); +} diff --git a/packages/interface/src/screens/Debug.tsx b/packages/interface/src/screens/Debug.tsx index 58d2480c6..b5788c203 100644 --- a/packages/interface/src/screens/Debug.tsx +++ b/packages/interface/src/screens/Debug.tsx @@ -17,8 +17,9 @@ export const DebugScreen: React.FC<{}> = (props) => { // }); const { mutate: identifyFiles } = useBridgeCommand('IdentifyUniqueFiles'); return ( -
-
+
+
+

Developer Debugger

*/} - - -
-
+ + +
+ {locations?.map((location) => ( + + ))} +
); } diff --git a/packages/ui/src/Button.tsx b/packages/ui/src/Button.tsx index 59e70e2f4..b523738e2 100644 --- a/packages/ui/src/Button.tsx +++ b/packages/ui/src/Button.tsx @@ -40,14 +40,12 @@ const variants = { dark:hover:bg-gray-500 dark:bg-opacity-80 dark:hover:bg-opacity-100 - dark:active:bg-gray-550 dark:active:opacity-80 border-gray-200 hover:border-gray-300 active:border-gray-200 dark:border-gray-500 - dark:active:border-gray-600 dark:hover:border-gray-500 text-gray-700 @@ -68,6 +66,12 @@ const variants = { hover:border-primary-500 active:border-primary-700 `, + colored: ` + text-white + shadow-sm + hover:bg-opacity-90 + active:bg-opacity-100 +`, selected: `bg-gray-100 dark:bg-gray-500 text-black hover:text-black active:text-black dark:hover:text-white dark:text-white ` @@ -114,7 +118,7 @@ export const Button = forwardRef< ) => { className = clsx( 'border rounded-md items-center transition-colors duration-100 cursor-default', - { 'opacity-5': loading, '!p-1': noPadding }, + { 'opacity-70': loading, '!p-1': noPadding }, { 'justify-center': !justifyLeft }, sizes[size || 'default'], variants[variant || 'default'],