mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 04:23:40 +00:00
rework installing page and add cancel install button (#2915)
* rework installing page and add cancel install button * actually call cancel endpoint * fix two bugs * include translations in progress component * cancellable installs * fix: comments (#2916) * fix: comments * delete comments * ensure trailing slash and no qp for new registry url --------- Co-authored-by: Matt Hill <mattnine@protonmail.com> * fix raspi * bump sdk --------- Co-authored-by: Aiden McClelland <me@drbonez.dev> Co-authored-by: Alex Inkin <alexander@inkin.ru>
This commit is contained in:
@@ -16,7 +16,7 @@ use models::{ActionId, PackageId};
|
|||||||
use reqwest::{Client, Proxy};
|
use reqwest::{Client, Proxy};
|
||||||
use rpc_toolkit::yajrc::RpcError;
|
use rpc_toolkit::yajrc::RpcError;
|
||||||
use rpc_toolkit::{CallRemote, Context, Empty};
|
use rpc_toolkit::{CallRemote, Context, Empty};
|
||||||
use tokio::sync::{broadcast, watch, Mutex, RwLock};
|
use tokio::sync::{broadcast, oneshot, watch, Mutex, RwLock};
|
||||||
use tokio::time::Instant;
|
use tokio::time::Instant;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
@@ -56,6 +56,7 @@ pub struct RpcContextSeed {
|
|||||||
pub os_net_service: NetService,
|
pub os_net_service: NetService,
|
||||||
pub s9pk_arch: Option<&'static str>,
|
pub s9pk_arch: Option<&'static str>,
|
||||||
pub services: ServiceMap,
|
pub services: ServiceMap,
|
||||||
|
pub cancellable_installs: SyncMutex<BTreeMap<PackageId, oneshot::Sender<()>>>,
|
||||||
pub metrics_cache: Watch<Option<crate::system::Metrics>>,
|
pub metrics_cache: Watch<Option<crate::system::Metrics>>,
|
||||||
pub shutdown: broadcast::Sender<Option<Shutdown>>,
|
pub shutdown: broadcast::Sender<Option<Shutdown>>,
|
||||||
pub tor_socks: SocketAddr,
|
pub tor_socks: SocketAddr,
|
||||||
@@ -239,6 +240,7 @@ impl RpcContext {
|
|||||||
Some(crate::ARCH)
|
Some(crate::ARCH)
|
||||||
},
|
},
|
||||||
services,
|
services,
|
||||||
|
cancellable_installs: SyncMutex::new(BTreeMap::new()),
|
||||||
metrics_cache,
|
metrics_cache,
|
||||||
shutdown,
|
shutdown,
|
||||||
tor_socks: tor_proxy,
|
tor_socks: tor_proxy,
|
||||||
|
|||||||
@@ -154,13 +154,15 @@ pub async fn install(
|
|||||||
})?
|
})?
|
||||||
.s9pk;
|
.s9pk;
|
||||||
|
|
||||||
|
let progress_tracker = FullProgressTracker::new();
|
||||||
|
let download_progress = progress_tracker.add_phase("Downloading".into(), Some(100));
|
||||||
let download = ctx
|
let download = ctx
|
||||||
.services
|
.services
|
||||||
.install(
|
.install(
|
||||||
ctx.clone(),
|
ctx.clone(),
|
||||||
|| asset.deserialize_s9pk_buffered(ctx.client.clone()),
|
|| asset.deserialize_s9pk_buffered(ctx.client.clone(), download_progress),
|
||||||
None::<Never>,
|
None::<Never>,
|
||||||
None,
|
Some(progress_tracker),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
tokio::spawn(async move { download.await?.await });
|
tokio::spawn(async move { download.await?.await });
|
||||||
@@ -188,10 +190,15 @@ pub async fn sideload(
|
|||||||
ctx: RpcContext,
|
ctx: RpcContext,
|
||||||
SideloadParams { session }: SideloadParams,
|
SideloadParams { session }: SideloadParams,
|
||||||
) -> Result<SideloadResponse, Error> {
|
) -> Result<SideloadResponse, Error> {
|
||||||
let (upload, file) = upload(&ctx, session.clone()).await?;
|
|
||||||
let (err_send, mut err_recv) = oneshot::channel::<Error>();
|
let (err_send, mut err_recv) = oneshot::channel::<Error>();
|
||||||
let progress = Guid::new();
|
let progress = Guid::new();
|
||||||
let progress_tracker = FullProgressTracker::new();
|
let progress_tracker = FullProgressTracker::new();
|
||||||
|
let (upload, file) = upload(
|
||||||
|
&ctx,
|
||||||
|
session.clone(),
|
||||||
|
progress_tracker.add_phase("Uploading".into(), Some(100)),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
let mut progress_listener = progress_tracker.stream(Some(Duration::from_millis(200)));
|
let mut progress_listener = progress_tracker.stream(Some(Duration::from_millis(200)));
|
||||||
ctx.rpc_continuations
|
ctx.rpc_continuations
|
||||||
.add(
|
.add(
|
||||||
@@ -268,6 +275,24 @@ pub async fn sideload(
|
|||||||
Ok(SideloadResponse { upload, progress })
|
Ok(SideloadResponse { upload, progress })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[command(rename_all = "kebab-case")]
|
||||||
|
pub struct CancelInstallParams {
|
||||||
|
pub id: PackageId,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
pub fn cancel_install(
|
||||||
|
ctx: RpcContext,
|
||||||
|
CancelInstallParams { id }: CancelInstallParams,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
if let Some(cancel) = ctx.cancellable_installs.mutate(|c| c.remove(&id)) {
|
||||||
|
cancel.send(()).ok();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Parser)]
|
#[derive(Deserialize, Serialize, Parser)]
|
||||||
pub struct QueryPackageParams {
|
pub struct QueryPackageParams {
|
||||||
id: PackageId,
|
id: PackageId,
|
||||||
|
|||||||
@@ -349,6 +349,13 @@ pub fn package<C: Context>() -> ParentHandler<C> {
|
|||||||
.no_display()
|
.no_display()
|
||||||
.with_about("Install a package from a marketplace or via sideloading"),
|
.with_about("Install a package from a marketplace or via sideloading"),
|
||||||
)
|
)
|
||||||
|
.subcommand(
|
||||||
|
"cancel-install",
|
||||||
|
from_fn(install::cancel_install)
|
||||||
|
.no_display()
|
||||||
|
.with_about("Cancel an install of a package")
|
||||||
|
.with_call_remote::<CliContext>(),
|
||||||
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"uninstall",
|
"uninstall",
|
||||||
from_fn_async(install::uninstall)
|
from_fn_async(install::uninstall)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use ts_rs::TS;
|
|||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
use crate::progress::PhaseProgressTrackerHandle;
|
||||||
use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment;
|
use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment;
|
||||||
use crate::registry::signer::commitment::{Commitment, Digestable};
|
use crate::registry::signer::commitment::{Commitment, Digestable};
|
||||||
use crate::registry::signer::sign::{AnySignature, AnyVerifyingKey};
|
use crate::registry::signer::sign::{AnySignature, AnyVerifyingKey};
|
||||||
@@ -75,9 +76,10 @@ impl RegistryAsset<MerkleArchiveCommitment> {
|
|||||||
pub async fn deserialize_s9pk_buffered(
|
pub async fn deserialize_s9pk_buffered(
|
||||||
&self,
|
&self,
|
||||||
client: Client,
|
client: Client,
|
||||||
|
progress: PhaseProgressTrackerHandle,
|
||||||
) -> Result<S9pk<Section<Arc<BufferedHttpSource>>>, Error> {
|
) -> Result<S9pk<Section<Arc<BufferedHttpSource>>>, Error> {
|
||||||
S9pk::deserialize(
|
S9pk::deserialize(
|
||||||
&Arc::new(BufferedHttpSource::new(client, self.url.clone()).await?),
|
&Arc::new(BufferedHttpSource::new(client, self.url.clone(), progress).await?),
|
||||||
Some(&self.commitment),
|
Some(&self.commitment),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -89,8 +91,12 @@ pub struct BufferedHttpSource {
|
|||||||
file: UploadingFile,
|
file: UploadingFile,
|
||||||
}
|
}
|
||||||
impl BufferedHttpSource {
|
impl BufferedHttpSource {
|
||||||
pub async fn new(client: Client, url: Url) -> Result<Self, Error> {
|
pub async fn new(
|
||||||
let (mut handle, file) = UploadingFile::new().await?;
|
client: Client,
|
||||||
|
url: Url,
|
||||||
|
progress: PhaseProgressTrackerHandle,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
let (mut handle, file) = UploadingFile::new(progress).await?;
|
||||||
let response = client.get(url).send().await?;
|
let response = client.get(url).send().await?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
_download: tokio::spawn(async move { handle.download(response).await }).into(),
|
_download: tokio::spawn(async move { handle.download(response).await }).into(),
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ use std::sync::Arc;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use color_eyre::eyre::eyre;
|
use color_eyre::eyre::eyre;
|
||||||
use futures::future::BoxFuture;
|
use futures::future::{BoxFuture, Fuse};
|
||||||
use futures::stream::FuturesUnordered;
|
use futures::stream::FuturesUnordered;
|
||||||
use futures::{Future, FutureExt, StreamExt};
|
use futures::{Future, FutureExt, StreamExt};
|
||||||
use helpers::NonDetachingJoinHandle;
|
use helpers::NonDetachingJoinHandle;
|
||||||
use imbl::OrdMap;
|
use imbl::OrdMap;
|
||||||
use imbl_value::InternedString;
|
use imbl_value::InternedString;
|
||||||
use models::ErrorData;
|
use models::ErrorData;
|
||||||
use tokio::sync::{Mutex, OwnedRwLockReadGuard, OwnedRwLockWriteGuard, RwLock};
|
use tokio::sync::{oneshot, Mutex, OwnedRwLockReadGuard, OwnedRwLockWriteGuard, RwLock};
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
use crate::context::RpcContext;
|
use crate::context::RpcContext;
|
||||||
@@ -138,41 +138,41 @@ impl ServiceMap {
|
|||||||
Fut: Future<Output = Result<S9pk<S>, Error>>,
|
Fut: Future<Output = Result<S9pk<S>, Error>>,
|
||||||
S: FileSource + Clone,
|
S: FileSource + Clone,
|
||||||
{
|
{
|
||||||
|
let progress = progress.unwrap_or_else(|| FullProgressTracker::new());
|
||||||
|
let mut validate_progress = progress.add_phase("Validating Headers".into(), Some(1));
|
||||||
|
let mut unpack_progress = progress.add_phase("Unpacking".into(), Some(100));
|
||||||
|
|
||||||
let mut s9pk = s9pk().await?;
|
let mut s9pk = s9pk().await?;
|
||||||
|
validate_progress.start();
|
||||||
s9pk.validate_and_filter(ctx.s9pk_arch)?;
|
s9pk.validate_and_filter(ctx.s9pk_arch)?;
|
||||||
|
validate_progress.complete();
|
||||||
let manifest = s9pk.as_manifest().clone();
|
let manifest = s9pk.as_manifest().clone();
|
||||||
let id = manifest.id.clone();
|
let id = manifest.id.clone();
|
||||||
let icon = s9pk.icon_data_url().await?;
|
let icon = s9pk.icon_data_url().await?;
|
||||||
let developer_key = s9pk.as_archive().signer();
|
let developer_key = s9pk.as_archive().signer();
|
||||||
let mut service = self.get_mut(&id).await;
|
let mut service = self.get_mut(&id).await;
|
||||||
|
let size = s9pk.size();
|
||||||
|
if let Some(size) = size {
|
||||||
|
unpack_progress.set_total(size);
|
||||||
|
}
|
||||||
let op_name = if recovery_source.is_none() {
|
let op_name = if recovery_source.is_none() {
|
||||||
if service.is_none() {
|
if service.is_none() {
|
||||||
"Install"
|
"Installing"
|
||||||
} else {
|
} else {
|
||||||
"Update"
|
"Updating"
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
"Restore"
|
"Restoring"
|
||||||
};
|
};
|
||||||
|
let mut finalization_progress = progress.add_phase(op_name.into(), Some(50));
|
||||||
let size = s9pk.size();
|
|
||||||
let progress = progress.unwrap_or_else(|| FullProgressTracker::new());
|
|
||||||
let download_progress_contribution = size.unwrap_or(60);
|
|
||||||
let mut download_progress = progress.add_phase(
|
|
||||||
InternedString::intern("Download"),
|
|
||||||
Some(download_progress_contribution),
|
|
||||||
);
|
|
||||||
if let Some(size) = size {
|
|
||||||
download_progress.set_total(size);
|
|
||||||
}
|
|
||||||
let mut finalization_progress = progress.add_phase(
|
|
||||||
InternedString::intern(op_name),
|
|
||||||
Some(download_progress_contribution / 2),
|
|
||||||
);
|
|
||||||
let restoring = recovery_source.is_some();
|
let restoring = recovery_source.is_some();
|
||||||
|
|
||||||
let mut reload_guard = ServiceRefReloadGuard::new(ctx.clone(), id.clone(), op_name);
|
let (cancel_send, cancel_recv) = oneshot::channel();
|
||||||
|
ctx.cancellable_installs
|
||||||
|
.mutate(|c| c.insert(id.clone(), cancel_send));
|
||||||
|
|
||||||
|
let mut reload_guard =
|
||||||
|
ServiceRefReloadCancelGuard::new(ctx.clone(), id.clone(), op_name, Some(cancel_recv));
|
||||||
|
|
||||||
reload_guard
|
reload_guard
|
||||||
.handle(async {
|
.handle(async {
|
||||||
@@ -256,15 +256,15 @@ impl ServiceMap {
|
|||||||
Some(Duration::from_millis(100)),
|
Some(Duration::from_millis(100)),
|
||||||
)));
|
)));
|
||||||
|
|
||||||
download_progress.start();
|
unpack_progress.start();
|
||||||
let mut progress_writer = ProgressTrackerWriter::new(
|
let mut progress_writer = ProgressTrackerWriter::new(
|
||||||
crate::util::io::create_file(&download_path).await?,
|
crate::util::io::create_file(&download_path).await?,
|
||||||
download_progress,
|
unpack_progress,
|
||||||
);
|
);
|
||||||
s9pk.serialize(&mut progress_writer, true).await?;
|
s9pk.serialize(&mut progress_writer, true).await?;
|
||||||
let (file, mut download_progress) = progress_writer.into_inner();
|
let (file, mut unpack_progress) = progress_writer.into_inner();
|
||||||
file.sync_all().await?;
|
file.sync_all().await?;
|
||||||
download_progress.complete();
|
unpack_progress.complete();
|
||||||
|
|
||||||
let installed_path = Path::new(DATA_DIR)
|
let installed_path = Path::new(DATA_DIR)
|
||||||
.join(PKG_ARCHIVE_DIR)
|
.join(PKG_ARCHIVE_DIR)
|
||||||
@@ -339,7 +339,7 @@ impl ServiceMap {
|
|||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let mut guard = self.get_mut(id).await;
|
let mut guard = self.get_mut(id).await;
|
||||||
if let Some(service) = guard.take() {
|
if let Some(service) = guard.take() {
|
||||||
ServiceRefReloadGuard::new(ctx.clone(), id.clone(), "Uninstall")
|
ServiceRefReloadCancelGuard::new(ctx.clone(), id.clone(), "Uninstall", None)
|
||||||
.handle_last(async move {
|
.handle_last(async move {
|
||||||
let res = service.uninstall(None, soft, force).await;
|
let res = service.uninstall(None, soft, force).await;
|
||||||
drop(guard);
|
drop(guard);
|
||||||
@@ -370,32 +370,51 @@ impl ServiceMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ServiceRefReloadGuard(Option<ServiceRefReloadInfo>);
|
pub struct ServiceRefReloadCancelGuard(
|
||||||
impl Drop for ServiceRefReloadGuard {
|
Option<ServiceRefReloadInfo>,
|
||||||
|
Option<Fuse<oneshot::Receiver<()>>>,
|
||||||
|
);
|
||||||
|
impl Drop for ServiceRefReloadCancelGuard {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
if let Some(info) = self.0.take() {
|
if let Some(info) = self.0.take() {
|
||||||
tokio::spawn(info.reload(None));
|
tokio::spawn(info.reload(None));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl ServiceRefReloadGuard {
|
impl ServiceRefReloadCancelGuard {
|
||||||
pub fn new(ctx: RpcContext, id: PackageId, operation: &'static str) -> Self {
|
pub fn new(
|
||||||
Self(Some(ServiceRefReloadInfo { ctx, id, operation }))
|
ctx: RpcContext,
|
||||||
|
id: PackageId,
|
||||||
|
operation: &'static str,
|
||||||
|
cancel: Option<oneshot::Receiver<()>>,
|
||||||
|
) -> Self {
|
||||||
|
Self(
|
||||||
|
Some(ServiceRefReloadInfo { ctx, id, operation }),
|
||||||
|
cancel.map(|c| c.fuse()),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle<T>(
|
pub async fn handle<T>(
|
||||||
&mut self,
|
&mut self,
|
||||||
operation: impl Future<Output = Result<T, Error>>,
|
operation: impl Future<Output = Result<T, Error>>,
|
||||||
) -> Result<T, Error> {
|
) -> Result<T, Error> {
|
||||||
let mut errors = ErrorCollection::new();
|
let res = async {
|
||||||
match operation.await {
|
if let Some(cancel) = self.1.as_mut() {
|
||||||
|
tokio::select! {
|
||||||
|
res = operation => res,
|
||||||
|
_ = cancel => Err(Error::new(eyre!("Operation Cancelled"), ErrorKind::Cancelled)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
operation.await
|
||||||
|
}
|
||||||
|
}.await;
|
||||||
|
match res {
|
||||||
Ok(a) => Ok(a),
|
Ok(a) => Ok(a),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if let Some(info) = self.0.take() {
|
if let Some(info) = self.0.take() {
|
||||||
errors.handle(info.reload(Some(e.clone_output())).await);
|
tokio::spawn(info.reload(Some(e.clone_output())));
|
||||||
}
|
}
|
||||||
errors.handle::<(), _>(Err(e));
|
Err(e)
|
||||||
errors.into_result().map(|_| unreachable!()) // TODO: there's gotta be a more elegant way?
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use tokio::sync::watch;
|
|||||||
|
|
||||||
use crate::context::RpcContext;
|
use crate::context::RpcContext;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
use crate::progress::PhaseProgressTrackerHandle;
|
||||||
use crate::rpc_continuations::{Guid, RpcContinuation};
|
use crate::rpc_continuations::{Guid, RpcContinuation};
|
||||||
use crate::s9pk::merkle_archive::source::multi_cursor_file::{FileCursor, MultiCursorFile};
|
use crate::s9pk::merkle_archive::source::multi_cursor_file::{FileCursor, MultiCursorFile};
|
||||||
use crate::s9pk::merkle_archive::source::ArchiveSource;
|
use crate::s9pk::merkle_archive::source::ArchiveSource;
|
||||||
@@ -26,9 +27,10 @@ use crate::util::io::{create_file, TmpDir};
|
|||||||
pub async fn upload(
|
pub async fn upload(
|
||||||
ctx: &RpcContext,
|
ctx: &RpcContext,
|
||||||
session: Option<InternedString>,
|
session: Option<InternedString>,
|
||||||
|
progress: PhaseProgressTrackerHandle,
|
||||||
) -> Result<(Guid, UploadingFile), Error> {
|
) -> Result<(Guid, UploadingFile), Error> {
|
||||||
let guid = Guid::new();
|
let guid = Guid::new();
|
||||||
let (mut handle, file) = UploadingFile::new().await?;
|
let (mut handle, file) = UploadingFile::new(progress).await?;
|
||||||
ctx.rpc_continuations
|
ctx.rpc_continuations
|
||||||
.add(
|
.add(
|
||||||
guid.clone(),
|
guid.clone(),
|
||||||
@@ -50,8 +52,8 @@ pub async fn upload(
|
|||||||
Ok((guid, file))
|
Ok((guid, file))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct Progress {
|
struct Progress {
|
||||||
|
tracker: PhaseProgressTrackerHandle,
|
||||||
expected_size: Option<u64>,
|
expected_size: Option<u64>,
|
||||||
written: u64,
|
written: u64,
|
||||||
error: Option<Error>,
|
error: Option<Error>,
|
||||||
@@ -69,6 +71,7 @@ impl Progress {
|
|||||||
match res {
|
match res {
|
||||||
Ok(a) => {
|
Ok(a) => {
|
||||||
self.written += *a as u64;
|
self.written += *a as u64;
|
||||||
|
self.tracker += *a as u64;
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
Err(e) => self.handle_error(e),
|
Err(e) => self.handle_error(e),
|
||||||
@@ -123,6 +126,7 @@ impl Progress {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn complete(&mut self) -> bool {
|
fn complete(&mut self) -> bool {
|
||||||
|
self.tracker.complete();
|
||||||
match self {
|
match self {
|
||||||
Self {
|
Self {
|
||||||
expected_size: Some(size),
|
expected_size: Some(size),
|
||||||
@@ -133,6 +137,7 @@ impl Progress {
|
|||||||
expected_size: Some(size),
|
expected_size: Some(size),
|
||||||
written,
|
written,
|
||||||
error,
|
error,
|
||||||
|
..
|
||||||
} if *written > *size && error.is_none() => {
|
} if *written > *size && error.is_none() => {
|
||||||
*error = Some(Error::new(
|
*error = Some(Error::new(
|
||||||
eyre!("Too many bytes received"),
|
eyre!("Too many bytes received"),
|
||||||
@@ -171,8 +176,13 @@ pub struct UploadingFile {
|
|||||||
progress: watch::Receiver<Progress>,
|
progress: watch::Receiver<Progress>,
|
||||||
}
|
}
|
||||||
impl UploadingFile {
|
impl UploadingFile {
|
||||||
pub async fn new() -> Result<(UploadHandle, Self), Error> {
|
pub async fn new(progress: PhaseProgressTrackerHandle) -> Result<(UploadHandle, Self), Error> {
|
||||||
let progress = watch::channel(Progress::default());
|
let progress = watch::channel(Progress {
|
||||||
|
tracker: progress,
|
||||||
|
expected_size: None,
|
||||||
|
written: 0,
|
||||||
|
error: None,
|
||||||
|
});
|
||||||
let tmp_dir = Arc::new(TmpDir::new().await?);
|
let tmp_dir = Arc::new(TmpDir::new().await?);
|
||||||
let file = create_file(tmp_dir.join("upload.tmp")).await?;
|
let file = create_file(tmp_dir.join("upload.tmp")).await?;
|
||||||
let uploading = Self {
|
let uploading = Self {
|
||||||
@@ -327,10 +337,12 @@ impl UploadHandle {
|
|||||||
self.process_headers(request.headers());
|
self.process_headers(request.headers());
|
||||||
self.process_body(request.into_body().into_data_stream())
|
self.process_body(request.into_body().into_data_stream())
|
||||||
.await;
|
.await;
|
||||||
|
self.progress.send_if_modified(|p| p.complete());
|
||||||
}
|
}
|
||||||
pub async fn download(&mut self, response: reqwest::Response) {
|
pub async fn download(&mut self, response: reqwest::Response) {
|
||||||
self.process_headers(response.headers());
|
self.process_headers(response.headers());
|
||||||
self.process_body(response.bytes_stream()).await;
|
self.process_body(response.bytes_stream()).await;
|
||||||
|
self.progress.send_if_modified(|p| p.complete());
|
||||||
}
|
}
|
||||||
fn process_headers(&mut self, headers: &HeaderMap) {
|
fn process_headers(&mut self, headers: &HeaderMap) {
|
||||||
if let Some(content_length) = headers
|
if let Some(content_length) = headers
|
||||||
@@ -338,8 +350,10 @@ impl UploadHandle {
|
|||||||
.and_then(|a| a.to_str().log_err())
|
.and_then(|a| a.to_str().log_err())
|
||||||
.and_then(|a| a.parse::<u64>().log_err())
|
.and_then(|a| a.parse::<u64>().log_err())
|
||||||
{
|
{
|
||||||
self.progress
|
self.progress.send_modify(|p| {
|
||||||
.send_modify(|p| p.expected_size = Some(content_length));
|
p.expected_size = Some(content_length);
|
||||||
|
p.tracker.set_total(content_length);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async fn process_body<E: Into<Box<(dyn std::error::Error + Send + Sync + 'static)>>>(
|
async fn process_body<E: Into<Box<(dyn std::error::Error + Send + Sync + 'static)>>>(
|
||||||
|
|||||||
@@ -206,8 +206,8 @@ if [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then
|
|||||||
echo "Configuring raspi kernel '\$v'"
|
echo "Configuring raspi kernel '\$v'"
|
||||||
extract-ikconfig "/usr/lib/modules/\$v/kernel/kernel/configs.ko.xz" > /boot/config-\$v
|
extract-ikconfig "/usr/lib/modules/\$v/kernel/kernel/configs.ko.xz" > /boot/config-\$v
|
||||||
done
|
done
|
||||||
mkinitramfs -c gzip -o /boot/initramfs8 6.12.20-v8+
|
mkinitramfs -c gzip -o /boot/initramfs8 6.12.25-v8+
|
||||||
mkinitramfs -c gzip -o /boot/initramfs_2712 6.12.20-v8-16k+
|
mkinitramfs -c gzip -o /boot/initramfs_2712 6.12.25-v8-16k+
|
||||||
fi
|
fi
|
||||||
|
|
||||||
useradd --shell /bin/bash -G startos -m start9
|
useradd --shell /bin/bash -G startos -m start9
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
import * as patterns from "../../base/lib/util/patterns"
|
import * as patterns from "../../base/lib/util/patterns"
|
||||||
import { BackupSync, Backups } from "./backup/Backups"
|
import { BackupSync, Backups } from "./backup/Backups"
|
||||||
import { smtpInputSpec } from "../../base/lib/actions/input/inputSpecConstants"
|
import { smtpInputSpec } from "../../base/lib/actions/input/inputSpecConstants"
|
||||||
import { CommandController, Daemons } from "./mainFn/Daemons"
|
import { CommandController, Daemon, Daemons } from "./mainFn/Daemons"
|
||||||
import { HealthCheck } from "./health/HealthCheck"
|
import { HealthCheck } from "./health/HealthCheck"
|
||||||
import { checkPortListening } from "./health/checkFns/checkPortListening"
|
import { checkPortListening } from "./health/checkFns/checkPortListening"
|
||||||
import { checkWebUrl, runHealthScript } from "./health/checkFns"
|
import { checkWebUrl, runHealthScript } from "./health/checkFns"
|
||||||
@@ -734,6 +734,11 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
|||||||
spec: Spec,
|
spec: Spec,
|
||||||
) => InputSpec.of<Spec, Store>(spec),
|
) => InputSpec.of<Spec, Store>(spec),
|
||||||
},
|
},
|
||||||
|
Daemon: {
|
||||||
|
get of() {
|
||||||
|
return Daemon.of<Manifest>()
|
||||||
|
},
|
||||||
|
},
|
||||||
Daemons: {
|
Daemons: {
|
||||||
of(
|
of(
|
||||||
effects: Effects,
|
effects: Effects,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as T from "../../../base/lib/types"
|
import * as T from "../../../base/lib/types"
|
||||||
import { asError } from "../../../base/lib/util/asError"
|
import { asError } from "../../../base/lib/util/asError"
|
||||||
|
import { Drop } from "../util"
|
||||||
import { ExecSpawnable, MountOptions, SubContainer } from "../util/SubContainer"
|
import { ExecSpawnable, MountOptions, SubContainer } from "../util/SubContainer"
|
||||||
import { CommandController } from "./CommandController"
|
import { CommandController } from "./CommandController"
|
||||||
import { Mounts } from "./Mounts"
|
import { Mounts } from "./Mounts"
|
||||||
@@ -11,12 +12,14 @@ const MAX_TIMEOUT_MS = 30000
|
|||||||
* and the others state of running, where it will keep a living running command
|
* and the others state of running, where it will keep a living running command
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export class Daemon<Manifest extends T.SDKManifest> {
|
export class Daemon<Manifest extends T.SDKManifest> extends Drop {
|
||||||
private commandController: CommandController<Manifest> | null = null
|
private commandController: CommandController<Manifest> | null = null
|
||||||
private shouldBeRunning = false
|
private shouldBeRunning = false
|
||||||
constructor(
|
constructor(
|
||||||
private startCommand: () => Promise<CommandController<Manifest>>,
|
private startCommand: () => Promise<CommandController<Manifest>>,
|
||||||
) {}
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
get subContainerHandle(): undefined | ExecSpawnable {
|
get subContainerHandle(): undefined | ExecSpawnable {
|
||||||
return this.commandController?.subContainerHandle
|
return this.commandController?.subContainerHandle
|
||||||
}
|
}
|
||||||
@@ -88,4 +91,7 @@ export class Daemon<Manifest extends T.SDKManifest> {
|
|||||||
.catch((e) => console.error(asError(e)))
|
.catch((e) => console.error(asError(e)))
|
||||||
this.commandController = null
|
this.commandController = null
|
||||||
}
|
}
|
||||||
|
onDrop(): void {
|
||||||
|
this.stop().catch((e) => console.error(asError(e)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,21 +51,26 @@ export type Ready = {
|
|||||||
type DaemonsParams<
|
type DaemonsParams<
|
||||||
Manifest extends T.SDKManifest,
|
Manifest extends T.SDKManifest,
|
||||||
Ids extends string,
|
Ids extends string,
|
||||||
Command extends string,
|
|
||||||
Id extends string,
|
Id extends string,
|
||||||
> = {
|
> =
|
||||||
/** The command line command to start the daemon */
|
| {
|
||||||
command: T.CommandType
|
/** The command line command to start the daemon */
|
||||||
/** Information about the subcontainer in which the daemon runs */
|
command: T.CommandType
|
||||||
subcontainer: SubContainer<Manifest>
|
/** Information about the subcontainer in which the daemon runs */
|
||||||
env?: Record<string, string>
|
subcontainer: SubContainer<Manifest>
|
||||||
ready: Ready
|
env?: Record<string, string>
|
||||||
/** An array of IDs of prior daemons whose successful initializations are required before this daemon will initialize */
|
ready: Ready
|
||||||
requires: Exclude<Ids, Id>[]
|
/** An array of IDs of prior daemons whose successful initializations are required before this daemon will initialize */
|
||||||
sigtermTimeout?: number
|
requires: Exclude<Ids, Id>[]
|
||||||
onStdout?: (chunk: Buffer | string | any) => void
|
sigtermTimeout?: number
|
||||||
onStderr?: (chunk: Buffer | string | any) => void
|
onStdout?: (chunk: Buffer | string | any) => void
|
||||||
}
|
onStderr?: (chunk: Buffer | string | any) => void
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
daemon: Daemon<Manifest>
|
||||||
|
ready: Ready
|
||||||
|
requires: Exclude<Ids, Id>[]
|
||||||
|
}
|
||||||
|
|
||||||
type ErrorDuplicateId<Id extends string> = `The id '${Id}' is already used`
|
type ErrorDuplicateId<Id extends string> = `The id '${Id}' is already used`
|
||||||
|
|
||||||
@@ -136,27 +141,23 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
|
|||||||
* @param newDaemon
|
* @param newDaemon
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
addDaemon<Id extends string, Command extends string>(
|
addDaemon<Id extends string>(
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
id:
|
id:
|
||||||
"" extends Id ? never :
|
"" extends Id ? never :
|
||||||
ErrorDuplicateId<Id> extends Id ? never :
|
ErrorDuplicateId<Id> extends Id ? never :
|
||||||
Id extends Ids ? ErrorDuplicateId<Id> :
|
Id extends Ids ? ErrorDuplicateId<Id> :
|
||||||
Id,
|
Id,
|
||||||
options: DaemonsParams<Manifest, Ids, Command, Id>,
|
options: DaemonsParams<Manifest, Ids, Id>,
|
||||||
) {
|
) {
|
||||||
const daemonIndex = this.daemons.length
|
const daemon =
|
||||||
const daemon = Daemon.of()(
|
"daemon" in options
|
||||||
this.effects,
|
? Promise.resolve(options.daemon)
|
||||||
options.subcontainer,
|
: Daemon.of()(this.effects, options.subcontainer, options.command, {
|
||||||
options.command,
|
...options,
|
||||||
{
|
})
|
||||||
...options,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
const healthDaemon = new HealthDaemon(
|
const healthDaemon = new HealthDaemon(
|
||||||
daemon,
|
daemon,
|
||||||
daemonIndex,
|
|
||||||
options.requires
|
options.requires
|
||||||
.map((x) => this.ids.indexOf(x))
|
.map((x) => this.ids.indexOf(x))
|
||||||
.filter((x) => x >= 0)
|
.filter((x) => x >= 0)
|
||||||
@@ -165,7 +166,6 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
|
|||||||
this.ids,
|
this.ids,
|
||||||
options.ready,
|
options.ready,
|
||||||
this.effects,
|
this.effects,
|
||||||
options.sigtermTimeout,
|
|
||||||
)
|
)
|
||||||
const daemons = this.daemons.concat(daemon)
|
const daemons = this.daemons.concat(daemon)
|
||||||
const ids = [...this.ids, id] as (Ids | Id)[]
|
const ids = [...this.ids, id] as (Ids | Id)[]
|
||||||
@@ -184,7 +184,7 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
|
|||||||
try {
|
try {
|
||||||
this.healthChecks.forEach((health) => health.stop())
|
this.healthChecks.forEach((health) => health.stop())
|
||||||
for (let result of await Promise.allSettled(
|
for (let result of await Promise.allSettled(
|
||||||
this.healthDaemons.map((x) => x.term({ timeout: x.sigtermTimeout })),
|
this.healthDaemons.map((x) => x.term()),
|
||||||
)) {
|
)) {
|
||||||
if (result.status === "rejected") {
|
if (result.status === "rejected") {
|
||||||
console.error(result.reason)
|
console.error(result.reason)
|
||||||
|
|||||||
@@ -30,13 +30,11 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
|||||||
private readyPromise: Promise<void>
|
private readyPromise: Promise<void>
|
||||||
constructor(
|
constructor(
|
||||||
private readonly daemon: Promise<Daemon<Manifest>>,
|
private readonly daemon: Promise<Daemon<Manifest>>,
|
||||||
readonly daemonIndex: number,
|
|
||||||
private readonly dependencies: HealthDaemon<Manifest>[],
|
private readonly dependencies: HealthDaemon<Manifest>[],
|
||||||
readonly id: string,
|
readonly id: string,
|
||||||
readonly ids: string[],
|
readonly ids: string[],
|
||||||
readonly ready: Ready,
|
readonly ready: Ready,
|
||||||
readonly effects: Effects,
|
readonly effects: Effects,
|
||||||
readonly sigtermTimeout: number = DEFAULT_SIGTERM_TIMEOUT,
|
|
||||||
) {
|
) {
|
||||||
this.readyPromise = new Promise((resolve) => (this.resolveReady = resolve))
|
this.readyPromise = new Promise((resolve) => (this.resolveReady = resolve))
|
||||||
this.dependencies.forEach((d) => d.addWatcher(() => this.updateStatus()))
|
this.dependencies.forEach((d) => d.addWatcher(() => this.updateStatus()))
|
||||||
@@ -53,7 +51,6 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
|||||||
|
|
||||||
await this.daemon.then((d) =>
|
await this.daemon.then((d) =>
|
||||||
d.term({
|
d.term({
|
||||||
timeout: this.sigtermTimeout,
|
|
||||||
...termOptions,
|
...termOptions,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
4
sdk/package/package-lock.json
generated
4
sdk/package/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@start9labs/start-sdk",
|
"name": "@start9labs/start-sdk",
|
||||||
"version": "0.4.0-beta.11",
|
"version": "0.4.0-beta.12",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@start9labs/start-sdk",
|
"name": "@start9labs/start-sdk",
|
||||||
"version": "0.4.0-beta.11",
|
"version": "0.4.0-beta.12",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iarna/toml": "^3.0.0",
|
"@iarna/toml": "^3.0.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@start9labs/start-sdk",
|
"name": "@start9labs/start-sdk",
|
||||||
"version": "0.4.0-beta.11",
|
"version": "0.4.0-beta.12",
|
||||||
"description": "Software development kit to facilitate packaging services for StartOS",
|
"description": "Software development kit to facilitate packaging services for StartOS",
|
||||||
"main": "./package/lib/index.js",
|
"main": "./package/lib/index.js",
|
||||||
"types": "./package/lib/index.d.ts",
|
"types": "./package/lib/index.d.ts",
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export class AppComponent {
|
|||||||
)
|
)
|
||||||
.subscribe({
|
.subscribe({
|
||||||
complete: async () => {
|
complete: async () => {
|
||||||
const loader = this.loader.open('' as i18nKey).subscribe()
|
const loader = this.loader.open().subscribe()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.api.reboot()
|
await this.api.reboot()
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import { StoreIconComponentModule } from './store-icon/store-icon.component.modu
|
|||||||
<ng-content />
|
<ng-content />
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
styles: [':host { border-radius: 0.25rem; width: stretch; }'],
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [StoreIconComponentModule, TuiIcon, TuiTitle],
|
imports: [StoreIconComponentModule, TuiIcon, TuiTitle],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export type StoreIdentity = {
|
|||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Marketplace = Record<string, StoreData | null>
|
export type Marketplace = Record<string, StoreDataWithUrl | null>
|
||||||
|
|
||||||
export type StoreData = {
|
export type StoreData = {
|
||||||
info: T.RegistryInfo
|
info: T.RegistryInfo
|
||||||
|
|||||||
@@ -362,8 +362,8 @@ export default {
|
|||||||
359: 'Die Partition enthält keine gültige Sicherung',
|
359: 'Die Partition enthält keine gültige Sicherung',
|
||||||
360: 'Sicherungsfortschritt',
|
360: 'Sicherungsfortschritt',
|
||||||
361: 'Abgeschlossen',
|
361: 'Abgeschlossen',
|
||||||
362: 'Sicherung läuft',
|
362: 'sicherung läuft',
|
||||||
363: 'Warten',
|
363: 'warten',
|
||||||
364: 'Sicherung erstellt',
|
364: 'Sicherung erstellt',
|
||||||
365: 'Wiederherstellung ausgewählt',
|
365: 'Wiederherstellung ausgewählt',
|
||||||
366: 'Initialisierung',
|
366: 'Initialisierung',
|
||||||
@@ -493,4 +493,9 @@ export default {
|
|||||||
490: 'deutsch',
|
490: 'deutsch',
|
||||||
491: 'englisch',
|
491: 'englisch',
|
||||||
492: 'Startmenü',
|
492: 'Startmenü',
|
||||||
|
493: 'Installationsfortschritt',
|
||||||
|
494: 'Herunterladen',
|
||||||
|
495: 'Validierung',
|
||||||
|
496: 'in Bearbeitung',
|
||||||
|
497: 'abgeschlossen',
|
||||||
} satisfies i18n
|
} satisfies i18n
|
||||||
|
|||||||
@@ -361,8 +361,8 @@ export const ENGLISH = {
|
|||||||
'Drive partition does not contain a valid backup': 359,
|
'Drive partition does not contain a valid backup': 359,
|
||||||
'Backup Progress': 360,
|
'Backup Progress': 360,
|
||||||
'Complete': 361,
|
'Complete': 361,
|
||||||
'Backing up': 362,
|
'backing up': 362,
|
||||||
'Waiting': 363,
|
'waiting': 363,
|
||||||
'Backup made': 364,
|
'Backup made': 364,
|
||||||
'Restore selected': 365,
|
'Restore selected': 365,
|
||||||
'Initializing': 366,
|
'Initializing': 366,
|
||||||
@@ -492,4 +492,9 @@ export const ENGLISH = {
|
|||||||
'german': 490,
|
'german': 490,
|
||||||
'english': 491,
|
'english': 491,
|
||||||
'Start Menu': 492,
|
'Start Menu': 492,
|
||||||
|
'Install Progress': 493,
|
||||||
|
'Downloading': 494,
|
||||||
|
'Validating': 495,
|
||||||
|
'in progress': 496,
|
||||||
|
'complete': 497,
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
@@ -362,8 +362,8 @@ export default {
|
|||||||
359: 'La partición de la unidad no contiene una copia de seguridad válida',
|
359: 'La partición de la unidad no contiene una copia de seguridad válida',
|
||||||
360: 'Progreso de la copia de seguridad',
|
360: 'Progreso de la copia de seguridad',
|
||||||
361: 'Completo',
|
361: 'Completo',
|
||||||
362: 'Haciendo copia de seguridad',
|
362: 'haciendo copia de seguridad',
|
||||||
363: 'Esperando',
|
363: 'esperando',
|
||||||
364: 'Copia de seguridad realizada',
|
364: 'Copia de seguridad realizada',
|
||||||
365: 'Restauración seleccionada',
|
365: 'Restauración seleccionada',
|
||||||
366: 'Inicializando',
|
366: 'Inicializando',
|
||||||
@@ -493,4 +493,9 @@ export default {
|
|||||||
490: 'alemán',
|
490: 'alemán',
|
||||||
491: 'inglés',
|
491: 'inglés',
|
||||||
492: 'Menú de Inicio',
|
492: 'Menú de Inicio',
|
||||||
} as any satisfies i18n
|
493: 'Progreso de instalación',
|
||||||
|
494: 'Descargando',
|
||||||
|
495: 'Validando',
|
||||||
|
496: 'en progreso',
|
||||||
|
497: 'completo',
|
||||||
|
} satisfies i18n
|
||||||
|
|||||||
@@ -362,8 +362,8 @@ export default {
|
|||||||
359: 'Partycja dysku nie zawiera prawidłowej kopii zapasowej',
|
359: 'Partycja dysku nie zawiera prawidłowej kopii zapasowej',
|
||||||
360: 'Postęp tworzenia kopii zapasowej',
|
360: 'Postęp tworzenia kopii zapasowej',
|
||||||
361: 'Zakończono',
|
361: 'Zakończono',
|
||||||
362: 'Tworzenie kopii zapasowej',
|
362: 'tworzenie kopii zapasowej',
|
||||||
363: 'Oczekiwanie',
|
363: 'oczekiwanie',
|
||||||
364: 'Kopia zapasowa utworzona',
|
364: 'Kopia zapasowa utworzona',
|
||||||
365: 'Wybrano przywracanie',
|
365: 'Wybrano przywracanie',
|
||||||
366: 'Inicjalizacja',
|
366: 'Inicjalizacja',
|
||||||
@@ -493,4 +493,9 @@ export default {
|
|||||||
490: 'niemiecki',
|
490: 'niemiecki',
|
||||||
491: 'angielski',
|
491: 'angielski',
|
||||||
492: 'Menu Startowe',
|
492: 'Menu Startowe',
|
||||||
|
493: 'Postęp instalacji',
|
||||||
|
494: 'Pobieranie',
|
||||||
|
495: 'Weryfikowanie',
|
||||||
|
496: 'w toku',
|
||||||
|
497: 'zakończono',
|
||||||
} satisfies i18n
|
} satisfies i18n
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class LoadingComponent {
|
|||||||
useFactory: () => new LoadingService(TUI_DIALOGS, LoadingComponent),
|
useFactory: () => new LoadingService(TUI_DIALOGS, LoadingComponent),
|
||||||
})
|
})
|
||||||
export class LoadingService extends TuiPopoverService<unknown> {
|
export class LoadingService extends TuiPopoverService<unknown> {
|
||||||
override open<G = void>(textContent: i18nKey) {
|
override open<G = void>(textContent: i18nKey | '' = '') {
|
||||||
return super.open<G>(textContent)
|
return super.open<G>(textContent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ export class MarketplaceControlsComponent {
|
|||||||
async tryInstall() {
|
async tryInstall() {
|
||||||
const currentUrl = this.file
|
const currentUrl = this.file
|
||||||
? null
|
? null
|
||||||
: await firstValueFrom(this.marketplaceService.getCurrentRegistryUrl$())
|
: await firstValueFrom(this.marketplaceService.currentRegistryUrl$)
|
||||||
const originalUrl = this.localPkg?.registry || null
|
const originalUrl = this.localPkg?.registry || null
|
||||||
|
|
||||||
if (!this.localPkg) {
|
if (!this.localPkg) {
|
||||||
|
|||||||
@@ -53,8 +53,7 @@ import { DialogService, i18nPipe } from '@start9labs/shared'
|
|||||||
})
|
})
|
||||||
export class MarketplaceMenuComponent {
|
export class MarketplaceMenuComponent {
|
||||||
private readonly dialog = inject(DialogService)
|
private readonly dialog = inject(DialogService)
|
||||||
private readonly marketplaceService = inject(MarketplaceService)
|
readonly registry$ = inject(MarketplaceService).currentRegistry$
|
||||||
readonly registry$ = this.marketplaceService.getCurrentRegistry$()
|
|
||||||
|
|
||||||
changeRegistry() {
|
changeRegistry() {
|
||||||
this.dialog
|
this.dialog
|
||||||
|
|||||||
@@ -29,9 +29,7 @@ import { StorageService } from 'src/app/services/storage.service'
|
|||||||
<div class="marketplace-content-inner">
|
<div class="marketplace-content-inner">
|
||||||
<marketplace-notification [url]="(url$ | async) || ''" />
|
<marketplace-notification [url]="(url$ | async) || ''" />
|
||||||
<div class="title-wrapper">
|
<div class="title-wrapper">
|
||||||
<h1>
|
<h1>{{ category$ | async | titlecase }}</h1>
|
||||||
{{ category$ | async | titlecase }}
|
|
||||||
</h1>
|
|
||||||
</div>
|
</div>
|
||||||
@if (registry$ | async; as registry) {
|
@if (registry$ | async; as registry) {
|
||||||
<section class="marketplace-content-list">
|
<section class="marketplace-content-list">
|
||||||
@@ -178,14 +176,14 @@ export default class MarketplaceComponent {
|
|||||||
queryParamsHandling: 'merge',
|
queryParamsHandling: 'merge',
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.marketplaceService.setRegistryUrl(registry)
|
this.marketplaceService.currentRegistryUrl$.next(registry)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.subscribe()
|
.subscribe()
|
||||||
|
|
||||||
readonly url$ = this.marketplaceService.getCurrentRegistryUrl$()
|
readonly url$ = this.marketplaceService.currentRegistryUrl$
|
||||||
readonly category$ = this.categoryService.getCategory$()
|
readonly category$ = this.categoryService.getCategory$()
|
||||||
readonly query$ = this.categoryService.getQuery$()
|
readonly query$ = this.categoryService.getQuery$()
|
||||||
readonly registry$ = this.marketplaceService.getCurrentRegistry$()
|
readonly registry$ = this.marketplaceService.currentRegistry$
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ export class MarketplacePreviewComponent {
|
|||||||
|
|
||||||
readonly flavors$ = this.flavor$.pipe(
|
readonly flavors$ = this.flavor$.pipe(
|
||||||
switchMap(current =>
|
switchMap(current =>
|
||||||
this.marketplaceService.getCurrentRegistry$().pipe(
|
this.marketplaceService.currentRegistry$.pipe(
|
||||||
map(({ packages }) =>
|
map(({ packages }) =>
|
||||||
packages.filter(
|
packages.filter(
|
||||||
({ id, flavor }) => id === this.pkgId && flavor !== current,
|
({ id, flavor }) => id === this.pkgId && flavor !== current,
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ import { StorageService } from 'src/app/services/storage.service'
|
|||||||
></button>
|
></button>
|
||||||
}
|
}
|
||||||
<h3 class="g-title">{{ 'Custom Registries' | i18n }}</h3>
|
<h3 class="g-title">{{ 'Custom Registries' | i18n }}</h3>
|
||||||
<button tuiCell (click)="add()" [style.width]="'-webkit-fill-available'">
|
<button tuiCell (click)="add()">
|
||||||
<tui-icon icon="@tui.plus" [style.margin-inline.rem]="'0.5'" />
|
<tui-icon icon="@tui.plus" [style.margin-inline.rem]="'0.5'" />
|
||||||
<div tuiTitle>{{ 'Add custom registry' | i18n }}</div>
|
<div tuiTitle>{{ 'Add custom registry' | i18n }}</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -71,6 +71,10 @@ import { StorageService } from 'src/app/services/storage.service'
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[tuiCell] {
|
||||||
|
width: stretch;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
],
|
],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
@@ -102,8 +106,8 @@ export class MarketplaceRegistryModal {
|
|||||||
private readonly storage = inject(StorageService)
|
private readonly storage = inject(StorageService)
|
||||||
|
|
||||||
readonly registries$ = combineLatest([
|
readonly registries$ = combineLatest([
|
||||||
this.marketplaceService.getRegistries$(),
|
this.marketplaceService.registries$,
|
||||||
this.marketplaceService.getCurrentRegistryUrl$(),
|
this.marketplaceService.currentRegistryUrl$,
|
||||||
]).pipe(
|
]).pipe(
|
||||||
map(([registries, currentUrl]) =>
|
map(([registries, currentUrl]) =>
|
||||||
registries.map(s => ({
|
registries.map(s => ({
|
||||||
@@ -185,7 +189,7 @@ export class MarketplaceRegistryModal {
|
|||||||
loader.closed = false
|
loader.closed = false
|
||||||
loader.add(this.loader.open('Changing registry').subscribe())
|
loader.add(this.loader.open('Changing registry').subscribe())
|
||||||
try {
|
try {
|
||||||
this.marketplaceService.setRegistryUrl(url)
|
this.marketplaceService.currentRegistryUrl$.next(url)
|
||||||
this.router.navigate([], {
|
this.router.navigate([], {
|
||||||
queryParams: { registry: url },
|
queryParams: { registry: url },
|
||||||
queryParamsHandling: 'merge',
|
queryParamsHandling: 'merge',
|
||||||
@@ -231,7 +235,7 @@ export class MarketplaceRegistryModal {
|
|||||||
|
|
||||||
private async save(rawUrl: string, connect = false): Promise<boolean> {
|
private async save(rawUrl: string, connect = false): Promise<boolean> {
|
||||||
const loader = this.loader.open('Loading').subscribe()
|
const loader = this.loader.open('Loading').subscribe()
|
||||||
const url = new URL(rawUrl).toString()
|
const url = new URL(rawUrl).origin + '/'
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.validateAndSave(url, loader)
|
await this.validateAndSave(url, loader)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { inject, Injectable } from '@angular/core'
|
import { inject, Injectable } from '@angular/core'
|
||||||
import { MarketplacePkgBase } from '@start9labs/marketplace'
|
import { MarketplacePkgBase } from '@start9labs/marketplace'
|
||||||
|
import { DialogService, i18nKey, i18nPipe, sameUrl } from '@start9labs/shared'
|
||||||
import { defaultIfEmpty, firstValueFrom } from 'rxjs'
|
import { defaultIfEmpty, firstValueFrom } from 'rxjs'
|
||||||
import { DialogService, i18nKey, i18nPipe } from '@start9labs/shared'
|
|
||||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@@ -16,14 +16,12 @@ export class MarketplaceAlertsService {
|
|||||||
url: string,
|
url: string,
|
||||||
originalUrl: string | null,
|
originalUrl: string | null,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const registries = await firstValueFrom(
|
const registries = await firstValueFrom(this.marketplaceService.registries$)
|
||||||
this.marketplaceService.getRegistries$(),
|
|
||||||
)
|
|
||||||
const message = originalUrl
|
const message = originalUrl
|
||||||
? `${this.i18n.transform('installed from')} ${registries.find(h => h.url === originalUrl) || originalUrl}`
|
? `${this.i18n.transform('installed from')} ${registries.find(r => sameUrl(r.url, originalUrl))?.name || originalUrl}`
|
||||||
: this.i18n.transform('sideloaded')
|
: this.i18n.transform('sideloaded')
|
||||||
|
|
||||||
const currentName = registries.find(h => h.url === url) || url
|
const currentName = registries.find(h => sameUrl(h.url, url))?.name || url
|
||||||
|
|
||||||
return new Promise(async resolve => {
|
return new Promise(async resolve => {
|
||||||
this.dialog
|
this.dialog
|
||||||
|
|||||||
@@ -1,31 +1,107 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
inject,
|
||||||
|
Input,
|
||||||
|
} from '@angular/core'
|
||||||
|
import { ErrorService, i18nPipe, LoadingService } from '@start9labs/shared'
|
||||||
import { T } from '@start9labs/start-sdk'
|
import { T } from '@start9labs/start-sdk'
|
||||||
|
import { TuiLet } from '@taiga-ui/cdk'
|
||||||
|
import { TuiButton } from '@taiga-ui/core'
|
||||||
import { TuiProgress } from '@taiga-ui/kit'
|
import { TuiProgress } from '@taiga-ui/kit'
|
||||||
import { InstallingProgressPipe } from 'src/app/routes/portal/routes/services/pipes/install-progress.pipe'
|
import { InstallingProgressPipe } from 'src/app/routes/portal/routes/services/pipes/install-progress.pipe'
|
||||||
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
|
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||||
|
import { getManifest } from 'src/app/utils/get-package-data'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: '[progress]',
|
selector: 'service-install-progress',
|
||||||
template: `
|
template: `
|
||||||
<ng-content />
|
<header>
|
||||||
@if (progress | installingProgress; as percent) {
|
{{ 'Install Progress' | i18n }}
|
||||||
: {{ percent }}%
|
<button
|
||||||
<progress
|
tuiButton
|
||||||
tuiProgressBar
|
|
||||||
size="xs"
|
size="xs"
|
||||||
[style.color]="
|
appearance="primary-destructive"
|
||||||
progress === true
|
[style.margin-inline-start]="'auto'"
|
||||||
? 'var(--tui-text-positive)'
|
(click)="cancel()"
|
||||||
: 'var(--tui-text-action)'
|
>
|
||||||
"
|
{{ 'Cancel' | i18n }}
|
||||||
[value]="percent / 100"
|
</button>
|
||||||
></progress>
|
</header>
|
||||||
|
|
||||||
|
@for (
|
||||||
|
phase of pkg.stateInfo.installingInfo?.progress?.phases;
|
||||||
|
track $index
|
||||||
|
) {
|
||||||
|
<div *tuiLet="phase.progress | installingProgress as percent">
|
||||||
|
{{ $any(phase.name) | i18n }}:
|
||||||
|
@if (phase.progress === null) {
|
||||||
|
<span>{{ 'waiting' | i18n }}</span>
|
||||||
|
} @else if (phase.progress === true) {
|
||||||
|
<span>{{ 'complete' | i18n }}!</span>
|
||||||
|
} @else if (phase.progress === false || phase.progress.total === null) {
|
||||||
|
<span>{{ 'in progress' | i18n }}...</span>
|
||||||
|
} @else {
|
||||||
|
<span>{{ percent }}%</span>
|
||||||
|
}
|
||||||
|
<progress
|
||||||
|
tuiProgressBar
|
||||||
|
size="m"
|
||||||
|
[max]="100"
|
||||||
|
[class.g-positive]="phase.progress === true"
|
||||||
|
[value]="isPending(phase.progress) ? undefined : percent"
|
||||||
|
></progress>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
styles: [':host { line-height: 2rem }'],
|
styles: `
|
||||||
|
:host {
|
||||||
|
grid-column: span 6;
|
||||||
|
color: var(--tui-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
float: right;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
host: { class: 'g-card' },
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [TuiProgress, InstallingProgressPipe],
|
imports: [TuiProgress, TuiLet, InstallingProgressPipe, i18nPipe, TuiButton],
|
||||||
})
|
})
|
||||||
export class ServiceProgressComponent {
|
export class ServiceInstallProgressComponent {
|
||||||
@Input({ required: true }) progress!: T.Progress
|
@Input({ required: true })
|
||||||
|
pkg!: PackageDataEntry
|
||||||
|
|
||||||
|
private readonly api = inject(ApiService)
|
||||||
|
private readonly errorService = inject(ErrorService)
|
||||||
|
private readonly loader = inject(LoadingService)
|
||||||
|
|
||||||
|
isPending(progress: T.Progress): boolean {
|
||||||
|
return (
|
||||||
|
!progress || (progress && progress !== true && progress.total === null)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async cancel() {
|
||||||
|
const loader = this.loader.open().subscribe()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.api.cancelInstallPackage({ id: getManifest(this.pkg).id })
|
||||||
|
} catch (e: any) {
|
||||||
|
this.errorService.handleError(e)
|
||||||
|
} finally {
|
||||||
|
loader.unsubscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
|
|||||||
@if (loading) {
|
@if (loading) {
|
||||||
<tui-loader size="s" />
|
<tui-loader size="s" />
|
||||||
} @else {
|
} @else {
|
||||||
@if (healthy) {
|
@if (!healthy) {
|
||||||
<tui-icon icon="@tui.check" class="g-positive" />
|
|
||||||
} @else {
|
|
||||||
<tui-icon icon="@tui.triangle-alert" class="g-warning" />
|
<tui-icon icon="@tui.triangle-alert" class="g-warning" />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,46 +25,45 @@ import { ServiceDependenciesComponent } from '../components/dependencies.compone
|
|||||||
import { ServiceErrorComponent } from '../components/error.component'
|
import { ServiceErrorComponent } from '../components/error.component'
|
||||||
import { ServiceHealthChecksComponent } from '../components/health-checks.component'
|
import { ServiceHealthChecksComponent } from '../components/health-checks.component'
|
||||||
import { ServiceInterfacesComponent } from '../components/interfaces.component'
|
import { ServiceInterfacesComponent } from '../components/interfaces.component'
|
||||||
import { ServiceProgressComponent } from '../components/progress.component'
|
import { ServiceInstallProgressComponent } from '../components/progress.component'
|
||||||
import { ServiceStatusComponent } from '../components/status.component'
|
import { ServiceStatusComponent } from '../components/status.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
<service-status
|
@if (pkg(); as pkg) {
|
||||||
[connected]="!!connected()"
|
@if (installing()) {
|
||||||
[installingInfo]="pkg()?.stateInfo?.installingInfo"
|
<service-install-progress [pkg]="pkg" />
|
||||||
[status]="status()"
|
} @else if (installed()) {
|
||||||
>
|
<service-status
|
||||||
@if ($any(pkg()?.status)?.started; as started) {
|
[connected]="!!connected()"
|
||||||
<p class="g-secondary" [appUptime]="started"></p>
|
[installingInfo]="pkg.stateInfo.installingInfo"
|
||||||
}
|
[status]="status()"
|
||||||
@if (installed() && connected() && pkg(); as pkg) {
|
>
|
||||||
<service-controls [pkg]="pkg" [status]="status()" />
|
@if ($any(pkg.status)?.started; as started) {
|
||||||
}
|
<p class="g-secondary" [appUptime]="started"></p>
|
||||||
</service-status>
|
}
|
||||||
|
|
||||||
@if (installed() && pkg(); as pkg) {
|
@if (connected()) {
|
||||||
@if (pkg.status.main === 'error') {
|
<service-controls [pkg]="pkg" [status]="status()" />
|
||||||
<service-error [pkg]="pkg" />
|
}
|
||||||
}
|
</service-status>
|
||||||
<service-interfaces [pkg]="pkg" [disabled]="status() !== 'running'" />
|
|
||||||
@if (errors() | async; as errors) {
|
|
||||||
<service-dependencies
|
|
||||||
[pkg]="pkg"
|
|
||||||
[services]="services()"
|
|
||||||
[errors]="errors"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
<service-health-checks [checks]="health()" />
|
|
||||||
<service-action-requests [pkg]="pkg" [services]="services() || {}" />
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (installing() && pkg(); as pkg) {
|
@if (pkg.status.main === 'error') {
|
||||||
@for (
|
<service-error [pkg]="pkg" />
|
||||||
item of pkg.stateInfo.installingInfo?.progress?.phases;
|
}
|
||||||
track $index
|
|
||||||
) {
|
<service-interfaces [pkg]="pkg" [disabled]="status() !== 'running'" />
|
||||||
<p [progress]="item.progress">{{ item.name }}</p>
|
|
||||||
|
@if (errors() | async; as errors) {
|
||||||
|
<service-dependencies
|
||||||
|
[pkg]="pkg"
|
||||||
|
[services]="services()"
|
||||||
|
[errors]="errors"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
<service-health-checks [checks]="health()" />
|
||||||
|
<service-action-requests [pkg]="pkg" [services]="services() || {}" />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
@@ -94,7 +93,7 @@ import { ServiceStatusComponent } from '../components/status.component'
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
ServiceProgressComponent,
|
ServiceInstallProgressComponent,
|
||||||
ServiceStatusComponent,
|
ServiceStatusComponent,
|
||||||
ServiceControlsComponent,
|
ServiceControlsComponent,
|
||||||
ServiceInterfacesComponent,
|
ServiceInterfacesComponent,
|
||||||
|
|||||||
@@ -27,13 +27,13 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
|||||||
<span tuiSubtitle>
|
<span tuiSubtitle>
|
||||||
@if (progress.complete) {
|
@if (progress.complete) {
|
||||||
<tui-icon icon="@tui.check" class="g-positive" />
|
<tui-icon icon="@tui.check" class="g-positive" />
|
||||||
{{ 'Complete' | i18n }}
|
{{ 'complete' | i18n }}
|
||||||
} @else {
|
} @else {
|
||||||
@if ((pkg.key | tuiMapper: toStatus | async) === 'backingUp') {
|
@if ((pkg.key | tuiMapper: toStatus | async) === 'backingUp') {
|
||||||
<tui-loader size="s" />
|
<tui-loader size="s" />
|
||||||
{{ 'Backing up' | i18n }}
|
{{ 'backing up' | i18n }}
|
||||||
} @else {
|
} @else {
|
||||||
{{ 'Waiting' | i18n }}...
|
{{ 'waiting' | i18n }}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -224,14 +224,12 @@ export default class UpdatesComponent {
|
|||||||
|
|
||||||
readonly data = toSignal<UpdatesData>(
|
readonly data = toSignal<UpdatesData>(
|
||||||
combineLatest({
|
combineLatest({
|
||||||
hosts: this.marketplaceService
|
hosts: this.marketplaceService.filteredRegistries$.pipe(
|
||||||
.getRegistries$(true)
|
tap(
|
||||||
.pipe(
|
([registry]) =>
|
||||||
tap(
|
!this.isMobile && registry && this.current.set(registry),
|
||||||
([registry]) =>
|
|
||||||
!this.isMobile && registry && this.current.set(registry),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
marketplace: this.marketplaceService.marketplace$,
|
marketplace: this.marketplaceService.marketplace$,
|
||||||
localPkgs: inject<PatchDB<DataModel>>(PatchDB)
|
localPkgs: inject<PatchDB<DataModel>>(PatchDB)
|
||||||
.watch$('packageData')
|
.watch$('packageData')
|
||||||
@@ -248,7 +246,7 @@ export default class UpdatesComponent {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
errors: this.marketplaceService.getRequestErrors$(),
|
errors: this.marketplaceService.requestErrors$,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -319,6 +319,9 @@ export namespace RR {
|
|||||||
export type InstallPackageReq = T.InstallParams
|
export type InstallPackageReq = T.InstallParams
|
||||||
export type InstallPackageRes = null
|
export type InstallPackageRes = null
|
||||||
|
|
||||||
|
export type CancelInstallPackageReq = { id: string }
|
||||||
|
export type CancelInstallPackageRes = null
|
||||||
|
|
||||||
export type GetActionInputReq = { packageId: string; actionId: string } // package.action.get-input
|
export type GetActionInputReq = { packageId: string; actionId: string } // package.action.get-input
|
||||||
export type GetActionInputRes = {
|
export type GetActionInputRes = {
|
||||||
spec: IST.InputSpec
|
spec: IST.InputSpec
|
||||||
|
|||||||
@@ -325,6 +325,10 @@ export abstract class ApiService {
|
|||||||
params: RR.InstallPackageReq,
|
params: RR.InstallPackageReq,
|
||||||
): Promise<RR.InstallPackageRes>
|
): Promise<RR.InstallPackageRes>
|
||||||
|
|
||||||
|
abstract cancelInstallPackage(
|
||||||
|
params: RR.CancelInstallPackageReq,
|
||||||
|
): Promise<RR.CancelInstallPackageRes>
|
||||||
|
|
||||||
abstract getActionInput(
|
abstract getActionInput(
|
||||||
params: RR.GetActionInputReq,
|
params: RR.GetActionInputReq,
|
||||||
): Promise<RR.GetActionInputRes>
|
): Promise<RR.GetActionInputRes>
|
||||||
|
|||||||
@@ -560,6 +560,12 @@ export class LiveApiService extends ApiService {
|
|||||||
return this.rpcRequest({ method: 'package.install', params })
|
return this.rpcRequest({ method: 'package.install', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async cancelInstallPackage(
|
||||||
|
params: RR.CancelInstallPackageReq,
|
||||||
|
): Promise<RR.CancelInstallPackageRes> {
|
||||||
|
return this.rpcRequest({ method: 'package.cancel-install', params })
|
||||||
|
}
|
||||||
|
|
||||||
async getActionInput(
|
async getActionInput(
|
||||||
params: RR.GetActionInputReq,
|
params: RR.GetActionInputReq,
|
||||||
): Promise<RR.GetActionInputRes> {
|
): Promise<RR.GetActionInputRes> {
|
||||||
|
|||||||
@@ -50,10 +50,7 @@ const PROGRESS: T.FullProgress = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Installing',
|
name: 'Installing',
|
||||||
progress: {
|
progress: null,
|
||||||
done: 0,
|
|
||||||
total: 40,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -1077,6 +1074,22 @@ export class MockApiService extends ApiService {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async cancelInstallPackage(
|
||||||
|
params: RR.CancelInstallPackageReq,
|
||||||
|
): Promise<RR.CancelInstallPackageRes> {
|
||||||
|
await pauseFor(500)
|
||||||
|
|
||||||
|
const patch: RemoveOperation[] = [
|
||||||
|
{
|
||||||
|
op: PatchOp.REMOVE,
|
||||||
|
path: `/packageData/${params.id}`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
this.mockRevision(patch)
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
async getActionInput(
|
async getActionInput(
|
||||||
params: RR.GetActionInputReq,
|
params: RR.GetActionInputReq,
|
||||||
): Promise<RR.GetActionInputRes> {
|
): Promise<RR.GetActionInputRes> {
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { inject, Injectable } from '@angular/core'
|
||||||
import {
|
import {
|
||||||
GetPackageRes,
|
GetPackageRes,
|
||||||
Marketplace,
|
Marketplace,
|
||||||
MarketplacePkg,
|
MarketplacePkg,
|
||||||
StoreData,
|
|
||||||
StoreDataWithUrl,
|
StoreDataWithUrl,
|
||||||
StoreIdentity,
|
StoreIdentity,
|
||||||
} from '@start9labs/marketplace'
|
} from '@start9labs/marketplace'
|
||||||
import { Exver, defaultRegistries, sameUrl } from '@start9labs/shared'
|
import { defaultRegistries, Exver, sameUrl } from '@start9labs/shared'
|
||||||
import { T } from '@start9labs/start-sdk'
|
import { T } from '@start9labs/start-sdk'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import {
|
import {
|
||||||
@@ -40,29 +39,11 @@ const { start9, community } = defaultRegistries
|
|||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class MarketplaceService {
|
export class MarketplaceService {
|
||||||
private readonly currentRegistryUrlSubject$ = new ReplaySubject<string>(1)
|
private readonly api = inject(ApiService)
|
||||||
private readonly currentRegistryUrl$ = this.currentRegistryUrlSubject$.pipe(
|
private readonly patch: PatchDB<DataModel> = inject(PatchDB)
|
||||||
distinctUntilChanged(),
|
private readonly exver = inject(Exver)
|
||||||
)
|
|
||||||
|
|
||||||
private readonly currentRegistry$: Observable<StoreDataWithUrl> =
|
readonly registries$: Observable<StoreIdentity[]> = this.patch
|
||||||
this.currentRegistryUrl$.pipe(
|
|
||||||
switchMap(url => this.fetchRegistry$(url)),
|
|
||||||
filter(Boolean),
|
|
||||||
map(registry => {
|
|
||||||
registry.info.categories = {
|
|
||||||
all: {
|
|
||||||
name: 'All',
|
|
||||||
},
|
|
||||||
...registry.info.categories,
|
|
||||||
}
|
|
||||||
|
|
||||||
return registry
|
|
||||||
}),
|
|
||||||
shareReplay(1),
|
|
||||||
)
|
|
||||||
|
|
||||||
private readonly registries$: Observable<StoreIdentity[]> = this.patch
|
|
||||||
.watch$('ui', 'registries')
|
.watch$('ui', 'registries')
|
||||||
.pipe(
|
.pipe(
|
||||||
map(registries => [
|
map(registries => [
|
||||||
@@ -74,21 +55,23 @@ export class MarketplaceService {
|
|||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
|
|
||||||
private readonly filteredRegistries$: Observable<StoreIdentity[]> =
|
// option to filter out hosts containing 'alpha' or 'beta' substrings in registryURL
|
||||||
combineLatest([
|
readonly filteredRegistries$: Observable<StoreIdentity[]> = combineLatest([
|
||||||
this.clientStorageService.showDevTools$,
|
inject(ClientStorageService).showDevTools$,
|
||||||
this.registries$,
|
this.registries$,
|
||||||
]).pipe(
|
]).pipe(
|
||||||
map(([devMode, registries]) =>
|
map(([devMode, registries]) =>
|
||||||
devMode
|
devMode
|
||||||
? registries
|
? registries
|
||||||
: registries.filter(
|
: registries.filter(
|
||||||
({ url }) => !url.includes('alpha') && !url.includes('beta'),
|
({ url }) => !url.includes('alpha') && !url.includes('beta'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
private readonly requestErrors$ = new BehaviorSubject<string[]>([])
|
readonly currentRegistryUrl$ = new ReplaySubject<string>(1)
|
||||||
|
|
||||||
|
readonly requestErrors$ = new BehaviorSubject<string[]>([])
|
||||||
|
|
||||||
readonly marketplace$: Observable<Marketplace> = this.registries$.pipe(
|
readonly marketplace$: Observable<Marketplace> = this.registries$.pipe(
|
||||||
startWith<StoreIdentity[]>([]),
|
startWith<StoreIdentity[]>([]),
|
||||||
@@ -102,11 +85,11 @@ export class MarketplaceService {
|
|||||||
if (data?.info.name)
|
if (data?.info.name)
|
||||||
this.updateRegistryName(url, name, data.info.name)
|
this.updateRegistryName(url, name, data.info.name)
|
||||||
}),
|
}),
|
||||||
map<StoreData | null, [string, StoreData | null]>(data => [url, data]),
|
map(data => [url, data] satisfies [string, StoreDataWithUrl | null]),
|
||||||
startWith<[string, StoreData | null]>([url, null]),
|
startWith<[string, StoreDataWithUrl | null]>([url, null]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
scan<[string, StoreData | null], Record<string, StoreData | null>>(
|
scan<[string, StoreDataWithUrl | null], Marketplace>(
|
||||||
(requests, [url, store]) => {
|
(requests, [url, store]) => {
|
||||||
requests[url] = store
|
requests[url] = store
|
||||||
|
|
||||||
@@ -114,32 +97,21 @@ export class MarketplaceService {
|
|||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
),
|
),
|
||||||
shareReplay({ bufferSize: 1, refCount: true }),
|
shareReplay(1),
|
||||||
)
|
)
|
||||||
|
|
||||||
constructor(
|
readonly currentRegistry$: Observable<StoreDataWithUrl> = combineLatest([
|
||||||
private readonly api: ApiService,
|
this.marketplace$,
|
||||||
private readonly patch: PatchDB<DataModel>,
|
this.currentRegistryUrl$,
|
||||||
private readonly clientStorageService: ClientStorageService,
|
this.currentRegistryUrl$.pipe(
|
||||||
private readonly exver: Exver,
|
distinctUntilChanged(),
|
||||||
) {}
|
switchMap(url => this.fetchRegistry$(url).pipe(startWith(null))),
|
||||||
|
),
|
||||||
getRegistries$(filtered = false): Observable<StoreIdentity[]> {
|
]).pipe(
|
||||||
// option to filter out hosts containing 'alpha' or 'beta' substrings in registryURL
|
map(([all, url, current]) => current || all[url]),
|
||||||
return filtered ? this.filteredRegistries$ : this.registries$
|
filter(Boolean),
|
||||||
}
|
shareReplay(1),
|
||||||
|
)
|
||||||
getCurrentRegistryUrl$() {
|
|
||||||
return this.currentRegistryUrl$
|
|
||||||
}
|
|
||||||
|
|
||||||
setRegistryUrl(url: string) {
|
|
||||||
this.currentRegistryUrlSubject$.next(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
getCurrentRegistry$(): Observable<StoreDataWithUrl> {
|
|
||||||
return this.currentRegistry$
|
|
||||||
}
|
|
||||||
|
|
||||||
getPackage$(
|
getPackage$(
|
||||||
id: string,
|
id: string,
|
||||||
@@ -161,14 +133,12 @@ export class MarketplaceService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchInfo$(url: string): Observable<T.RegistryInfo> {
|
fetchInfo$(registry: string): Observable<T.RegistryInfo> {
|
||||||
return from(this.api.getRegistryInfo({ registry: url })).pipe(
|
return from(this.api.getRegistryInfo({ registry })).pipe(
|
||||||
map(info => ({
|
map(info => ({
|
||||||
...info,
|
...info,
|
||||||
categories: {
|
categories: {
|
||||||
all: {
|
all: { name: 'All' },
|
||||||
name: 'All',
|
|
||||||
},
|
|
||||||
...info.categories,
|
...info.categories,
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
@@ -263,10 +233,6 @@ export class MarketplaceService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getRequestErrors$(): Observable<string[]> {
|
|
||||||
return this.requestErrors$
|
|
||||||
}
|
|
||||||
|
|
||||||
async installPackage(
|
async installPackage(
|
||||||
id: string,
|
id: string,
|
||||||
version: string,
|
version: string,
|
||||||
|
|||||||
Reference in New Issue
Block a user