mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +00:00
[Fix] websocket connecting and patchDB connection monitoring (#1738)
* refactor how we handle rpc responses and patchdb connection monitoring * websockets only * remove unused global error handlers * chore: clear storage inside auth service * feat: convert all global toasts to declarative approach (#1754) * no more reference to serverID Co-authored-by: Aiden McClelland <me@drbonez.dev> Co-authored-by: waterplea <alexander@inkin.ru>
This commit is contained in:
@@ -2,23 +2,22 @@ pub mod model;
|
|||||||
pub mod package;
|
pub mod package;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
||||||
use std::borrow::Cow;
|
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use color_eyre::eyre::eyre;
|
|
||||||
use futures::{FutureExt, SinkExt, StreamExt};
|
use futures::{FutureExt, SinkExt, StreamExt};
|
||||||
use patch_db::json_ptr::JsonPointer;
|
use patch_db::json_ptr::JsonPointer;
|
||||||
use patch_db::{Dump, Revision};
|
use patch_db::{Dump, Revision};
|
||||||
use rpc_toolkit::command;
|
use rpc_toolkit::command;
|
||||||
use rpc_toolkit::hyper::upgrade::Upgraded;
|
use rpc_toolkit::hyper::upgrade::Upgraded;
|
||||||
use rpc_toolkit::hyper::{Body, Error as HyperError, Request, Response};
|
use rpc_toolkit::hyper::{Body, Error as HyperError, Request, Response};
|
||||||
use rpc_toolkit::yajrc::{GenericRpcMethod, RpcError, RpcResponse};
|
use rpc_toolkit::yajrc::RpcError;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use tokio::sync::{broadcast, oneshot};
|
use tokio::sync::{broadcast, oneshot};
|
||||||
use tokio::task::JoinError;
|
use tokio::task::JoinError;
|
||||||
|
use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
|
||||||
|
use tokio_tungstenite::tungstenite::protocol::CloseFrame;
|
||||||
use tokio_tungstenite::tungstenite::Message;
|
use tokio_tungstenite::tungstenite::Message;
|
||||||
use tokio_tungstenite::WebSocketStream;
|
use tokio_tungstenite::WebSocketStream;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
@@ -30,11 +29,12 @@ use crate::middleware::auth::{HasValidSession, HashSessionToken};
|
|||||||
use crate::util::serde::{display_serializable, IoFormat};
|
use crate::util::serde::{display_serializable, IoFormat};
|
||||||
use crate::{Error, ResultExt};
|
use crate::{Error, ResultExt};
|
||||||
|
|
||||||
#[instrument(skip(ctx, ws_fut))]
|
#[instrument(skip(ctx, session, ws_fut))]
|
||||||
async fn ws_handler<
|
async fn ws_handler<
|
||||||
WSFut: Future<Output = Result<Result<WebSocketStream<Upgraded>, HyperError>, JoinError>>,
|
WSFut: Future<Output = Result<Result<WebSocketStream<Upgraded>, HyperError>, JoinError>>,
|
||||||
>(
|
>(
|
||||||
ctx: RpcContext,
|
ctx: RpcContext,
|
||||||
|
session: Option<(HasValidSession, HashSessionToken)>,
|
||||||
ws_fut: WSFut,
|
ws_fut: WSFut,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let (dump, sub) = ctx.db.dump_and_sub().await;
|
let (dump, sub) = ctx.db.dump_and_sub().await;
|
||||||
@@ -43,50 +43,21 @@ async fn ws_handler<
|
|||||||
.with_kind(crate::ErrorKind::Network)?
|
.with_kind(crate::ErrorKind::Network)?
|
||||||
.with_kind(crate::ErrorKind::Unknown)?;
|
.with_kind(crate::ErrorKind::Unknown)?;
|
||||||
|
|
||||||
let (has_valid_session, token) = loop {
|
if let Some((session, token)) = session {
|
||||||
if let Some(Message::Text(cookie)) = stream
|
let kill = subscribe_to_session_kill(&ctx, token).await;
|
||||||
.next()
|
send_dump(session, &mut stream, dump).await?;
|
||||||
|
|
||||||
|
deal_with_messages(session, kill, sub, stream).await?;
|
||||||
|
} else {
|
||||||
|
stream
|
||||||
|
.close(Some(CloseFrame {
|
||||||
|
code: CloseCode::Error,
|
||||||
|
reason: "UNAUTHORIZED".into(),
|
||||||
|
}))
|
||||||
.await
|
.await
|
||||||
.transpose()
|
.with_kind(crate::ErrorKind::Network)?;
|
||||||
.with_kind(crate::ErrorKind::Network)?
|
}
|
||||||
{
|
|
||||||
let cookie_str = serde_json::from_str::<Cow<str>>(&cookie)
|
|
||||||
.with_kind(crate::ErrorKind::Deserialization)?;
|
|
||||||
|
|
||||||
let id = basic_cookies::Cookie::parse(&cookie_str)
|
|
||||||
.with_kind(crate::ErrorKind::Authorization)?
|
|
||||||
.into_iter()
|
|
||||||
.find(|c| c.get_name() == "session")
|
|
||||||
.ok_or_else(|| {
|
|
||||||
Error::new(eyre!("UNAUTHORIZED"), crate::ErrorKind::Authorization)
|
|
||||||
})?;
|
|
||||||
let authenticated_session = HashSessionToken::from_cookie(&id);
|
|
||||||
match HasValidSession::from_session(&authenticated_session, &ctx).await {
|
|
||||||
Err(e) => {
|
|
||||||
stream
|
|
||||||
.send(Message::Text(
|
|
||||||
serde_json::to_string(
|
|
||||||
&RpcResponse::<GenericRpcMethod<String>>::from_result(Err::<
|
|
||||||
_,
|
|
||||||
RpcError,
|
|
||||||
>(
|
|
||||||
e.into()
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.with_kind(crate::ErrorKind::Serialization)?,
|
|
||||||
))
|
|
||||||
.await
|
|
||||||
.with_kind(crate::ErrorKind::Network)?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
Ok(has_validation) => break (has_validation, authenticated_session),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let kill = subscribe_to_session_kill(&ctx, token).await;
|
|
||||||
send_dump(has_valid_session, &mut stream, dump).await?;
|
|
||||||
|
|
||||||
deal_with_messages(has_valid_session, kill, sub, stream).await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,39 +86,25 @@ async fn deal_with_messages(
|
|||||||
futures::select! {
|
futures::select! {
|
||||||
_ = (&mut kill).fuse() => {
|
_ = (&mut kill).fuse() => {
|
||||||
tracing::info!("Closing WebSocket: Reason: Session Terminated");
|
tracing::info!("Closing WebSocket: Reason: Session Terminated");
|
||||||
|
stream
|
||||||
|
.close(Some(CloseFrame {
|
||||||
|
code: CloseCode::Error,
|
||||||
|
reason: "UNAUTHORIZED".into(),
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
.with_kind(crate::ErrorKind::Network)?;
|
||||||
return Ok(())
|
return Ok(())
|
||||||
}
|
}
|
||||||
new_rev = sub.recv().fuse() => {
|
new_rev = sub.recv().fuse() => {
|
||||||
let rev = new_rev.with_kind(crate::ErrorKind::Database)?;
|
let rev = new_rev.with_kind(crate::ErrorKind::Database)?;
|
||||||
stream
|
stream
|
||||||
.send(Message::Text(
|
.send(Message::Text(serde_json::to_string(&rev).with_kind(crate::ErrorKind::Serialization)?))
|
||||||
serde_json::to_string(
|
|
||||||
&RpcResponse::<GenericRpcMethod<String>>::from_result(Ok::<_, RpcError>(
|
|
||||||
serde_json::to_value(&rev).with_kind(crate::ErrorKind::Serialization)?,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.with_kind(crate::ErrorKind::Serialization)?,
|
|
||||||
))
|
|
||||||
.await
|
.await
|
||||||
.with_kind(crate::ErrorKind::Network)?;
|
.with_kind(crate::ErrorKind::Network)?;
|
||||||
}
|
}
|
||||||
message = stream.next().fuse() => {
|
message = stream.next().fuse() => {
|
||||||
let message = message.transpose().with_kind(crate::ErrorKind::Network)?;
|
let message = message.transpose().with_kind(crate::ErrorKind::Network)?;
|
||||||
match message {
|
match message {
|
||||||
Some(Message::Ping(a)) => {
|
|
||||||
stream
|
|
||||||
.send(Message::Pong(a))
|
|
||||||
.await
|
|
||||||
.with_kind(crate::ErrorKind::Network)?;
|
|
||||||
}
|
|
||||||
Some(Message::Close(frame)) => {
|
|
||||||
if let Some(reason) = frame.as_ref() {
|
|
||||||
tracing::info!("Closing WebSocket: Reason: {} {}", reason.code, reason.reason);
|
|
||||||
} else {
|
|
||||||
tracing::info!("Closing WebSocket: Reason: Unknown");
|
|
||||||
}
|
|
||||||
return Ok(())
|
|
||||||
}
|
|
||||||
None => {
|
None => {
|
||||||
tracing::info!("Closing WebSocket: Stream Finished");
|
tracing::info!("Closing WebSocket: Stream Finished");
|
||||||
return Ok(())
|
return Ok(())
|
||||||
@@ -155,12 +112,6 @@ async fn deal_with_messages(
|
|||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ = tokio::time::sleep(Duration::from_secs(10)).fuse() => {
|
|
||||||
stream
|
|
||||||
.send(Message::Ping(Vec::new()))
|
|
||||||
.await
|
|
||||||
.with_kind(crate::ErrorKind::Network)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,13 +123,7 @@ async fn send_dump(
|
|||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
stream
|
stream
|
||||||
.send(Message::Text(
|
.send(Message::Text(
|
||||||
serde_json::to_string(&RpcResponse::<GenericRpcMethod<String>>::from_result(Ok::<
|
serde_json::to_string(&dump).with_kind(crate::ErrorKind::Serialization)?,
|
||||||
_,
|
|
||||||
RpcError,
|
|
||||||
>(
|
|
||||||
serde_json::to_value(&dump).with_kind(crate::ErrorKind::Serialization)?,
|
|
||||||
)))
|
|
||||||
.with_kind(crate::ErrorKind::Serialization)?,
|
|
||||||
))
|
))
|
||||||
.await
|
.await
|
||||||
.with_kind(crate::ErrorKind::Network)?;
|
.with_kind(crate::ErrorKind::Network)?;
|
||||||
@@ -187,11 +132,27 @@ async fn send_dump(
|
|||||||
|
|
||||||
pub async fn subscribe(ctx: RpcContext, req: Request<Body>) -> Result<Response<Body>, Error> {
|
pub async fn subscribe(ctx: RpcContext, req: Request<Body>) -> Result<Response<Body>, Error> {
|
||||||
let (parts, body) = req.into_parts();
|
let (parts, body) = req.into_parts();
|
||||||
|
let session = match async {
|
||||||
|
let token = HashSessionToken::from_request_parts(&parts)?;
|
||||||
|
let session = HasValidSession::from_session(&token, &ctx).await?;
|
||||||
|
Ok::<_, Error>((session, token))
|
||||||
|
}
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(a) => Some(a),
|
||||||
|
Err(e) => {
|
||||||
|
if e.kind != crate::ErrorKind::Authorization {
|
||||||
|
tracing::error!("Error Authenticating Websocket: {}", e);
|
||||||
|
tracing::debug!("{:?}", e);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
let req = Request::from_parts(parts, body);
|
let req = Request::from_parts(parts, body);
|
||||||
let (res, ws_fut) = hyper_ws_listener::create_ws(req).with_kind(crate::ErrorKind::Network)?;
|
let (res, ws_fut) = hyper_ws_listener::create_ws(req).with_kind(crate::ErrorKind::Network)?;
|
||||||
if let Some(ws_fut) = ws_fut {
|
if let Some(ws_fut) = ws_fut {
|
||||||
tokio::task::spawn(async move {
|
tokio::task::spawn(async move {
|
||||||
match ws_handler(ctx, ws_fut).await {
|
match ws_handler(ctx, session, ws_fut).await {
|
||||||
Ok(()) => (),
|
Ok(()) => (),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("WebSocket Closed: {}", e);
|
tracing::error!("WebSocket Closed: {}", e);
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
use std::ops::Deref;
|
use std::ops::{Deref, DerefMut};
|
||||||
use std::ops::DerefMut;
|
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
use std::time::{Duration, UNIX_EPOCH};
|
use std::time::{Duration, UNIX_EPOCH};
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use color_eyre::eyre::eyre;
|
use color_eyre::eyre::eyre;
|
||||||
use futures::stream::BoxStream;
|
use futures::stream::BoxStream;
|
||||||
use futures::Stream;
|
use futures::{FutureExt, SinkExt, Stream, StreamExt, TryStreamExt};
|
||||||
use futures::{FutureExt, SinkExt, StreamExt, TryStreamExt};
|
|
||||||
use hyper::upgrade::Upgraded;
|
use hyper::upgrade::Upgraded;
|
||||||
use hyper::Error as HyperError;
|
use hyper::Error as HyperError;
|
||||||
use rpc_toolkit::command;
|
use rpc_toolkit::command;
|
||||||
@@ -30,7 +28,8 @@ use crate::core::rpc_continuations::{RequestGuid, RpcContinuation};
|
|||||||
use crate::error::ResultExt;
|
use crate::error::ResultExt;
|
||||||
use crate::procedure::docker::DockerProcedure;
|
use crate::procedure::docker::DockerProcedure;
|
||||||
use crate::s9pk::manifest::PackageId;
|
use crate::s9pk::manifest::PackageId;
|
||||||
use crate::util::{display_none, serde::Reversible};
|
use crate::util::display_none;
|
||||||
|
use crate::util::serde::Reversible;
|
||||||
use crate::{Error, ErrorKind};
|
use crate::{Error, ErrorKind};
|
||||||
|
|
||||||
#[pin_project::pin_project]
|
#[pin_project::pin_project]
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ use rpc_toolkit::command_helpers::prelude::RequestParts;
|
|||||||
use rpc_toolkit::hyper::header::COOKIE;
|
use rpc_toolkit::hyper::header::COOKIE;
|
||||||
use rpc_toolkit::hyper::http::Error as HttpError;
|
use rpc_toolkit::hyper::http::Error as HttpError;
|
||||||
use rpc_toolkit::hyper::{Body, Request, Response};
|
use rpc_toolkit::hyper::{Body, Request, Response};
|
||||||
use rpc_toolkit::rpc_server_helpers::{noop3, to_response, DynMiddleware, DynMiddlewareStage2};
|
use rpc_toolkit::rpc_server_helpers::{
|
||||||
|
noop4, to_response, DynMiddleware, DynMiddlewareStage2, DynMiddlewareStage3,
|
||||||
|
};
|
||||||
use rpc_toolkit::yajrc::RpcMethod;
|
use rpc_toolkit::yajrc::RpcMethod;
|
||||||
use rpc_toolkit::Metadata;
|
use rpc_toolkit::Metadata;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -198,8 +200,7 @@ pub fn auth<M: Metadata>(ctx: RpcContext) -> DynMiddleware<M> {
|
|||||||
|_| StatusCode::OK,
|
|_| StatusCode::OK,
|
||||||
)?));
|
)?));
|
||||||
} else if rpc_req.method.as_str() == "auth.login" {
|
} else if rpc_req.method.as_str() == "auth.login" {
|
||||||
let mut guard = rate_limiter.lock().await;
|
let guard = rate_limiter.lock().await;
|
||||||
guard.0 += 1;
|
|
||||||
if guard.1.elapsed() < Duration::from_secs(20) {
|
if guard.1.elapsed() < Duration::from_secs(20) {
|
||||||
if guard.0 >= 3 {
|
if guard.0 >= 3 {
|
||||||
let (res_parts, _) = Response::new(()).into_parts();
|
let (res_parts, _) = Response::new(()).into_parts();
|
||||||
@@ -216,13 +217,25 @@ pub fn auth<M: Metadata>(ctx: RpcContext) -> DynMiddleware<M> {
|
|||||||
|_| StatusCode::OK,
|
|_| StatusCode::OK,
|
||||||
)?));
|
)?));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let m3: DynMiddlewareStage3 = Box::new(move |_, res| {
|
||||||
|
async move {
|
||||||
|
let mut guard = rate_limiter.lock().await;
|
||||||
|
if guard.1.elapsed() < Duration::from_secs(20) {
|
||||||
|
if res.is_err() {
|
||||||
|
guard.0 += 1;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
guard.0 = 0;
|
guard.0 = 0;
|
||||||
}
|
}
|
||||||
guard.1 = Instant::now();
|
guard.1 = Instant::now();
|
||||||
|
Ok(Ok(noop4()))
|
||||||
}
|
}
|
||||||
}
|
.boxed()
|
||||||
Ok(Ok(noop3()))
|
});
|
||||||
|
Ok(Ok(m3))
|
||||||
}
|
}
|
||||||
.boxed()
|
.boxed()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,11 +2,6 @@
|
|||||||
"useMocks": true,
|
"useMocks": true,
|
||||||
"targetArch": "aarch64",
|
"targetArch": "aarch64",
|
||||||
"ui": {
|
"ui": {
|
||||||
"patchDb": {
|
|
||||||
"poll": {
|
|
||||||
"cooldown": 10000
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"api": {
|
"api": {
|
||||||
"url": "rpc",
|
"url": "rpc",
|
||||||
"version": "v1"
|
"version": "v1"
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { HttpClientModule } from '@angular/common/http'
|
|||||||
import { ApiService } from './services/api/api.service'
|
import { ApiService } from './services/api/api.service'
|
||||||
import { MockApiService } from './services/api/mock-api.service'
|
import { MockApiService } from './services/api/mock-api.service'
|
||||||
import { LiveApiService } from './services/api/live-api.service'
|
import { LiveApiService } from './services/api/live-api.service'
|
||||||
import { GlobalErrorHandler } from './services/global-error-handler.service'
|
|
||||||
import { WorkspaceConfig } from '@start9labs/shared'
|
import { WorkspaceConfig } from '@start9labs/shared'
|
||||||
|
|
||||||
const { useMocks } = require('../../../../config.json') as WorkspaceConfig
|
const { useMocks } = require('../../../../config.json') as WorkspaceConfig
|
||||||
@@ -29,7 +28,6 @@ const { useMocks } = require('../../../../config.json') as WorkspaceConfig
|
|||||||
provide: ApiService,
|
provide: ApiService,
|
||||||
useClass: useMocks ? MockApiService : LiveApiService,
|
useClass: useMocks ? MockApiService : LiveApiService,
|
||||||
},
|
},
|
||||||
{ provide: ErrorHandler, useClass: GlobalErrorHandler },
|
|
||||||
],
|
],
|
||||||
bootstrap: [AppComponent],
|
bootstrap: [AppComponent],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
import { ErrorHandler, Injectable } from '@angular/core'
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class GlobalErrorHandler implements ErrorHandler {
|
|
||||||
|
|
||||||
handleError (error: any): void {
|
|
||||||
const chunkFailedMessage = /Loading chunk [\d]+ failed/
|
|
||||||
|
|
||||||
if (chunkFailedMessage.test(error.message)) {
|
|
||||||
window.location.reload()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
} from '@ionic/angular'
|
} from '@ionic/angular'
|
||||||
import { AppComponent } from './app.component'
|
import { AppComponent } from './app.component'
|
||||||
import { AppRoutingModule } from './app-routing.module'
|
import { AppRoutingModule } from './app-routing.module'
|
||||||
import { GlobalErrorHandler } from './services/global-error-handler.service'
|
|
||||||
import { SuccessPageModule } from './pages/success/success.module'
|
import { SuccessPageModule } from './pages/success/success.module'
|
||||||
import { HomePageModule } from './pages/home/home.module'
|
import { HomePageModule } from './pages/home/home.module'
|
||||||
import { LoadingPageModule } from './pages/loading/loading.module'
|
import { LoadingPageModule } from './pages/loading/loading.module'
|
||||||
@@ -46,7 +45,6 @@ const { useMocks } = require('../../../../config.json') as WorkspaceConfig
|
|||||||
provide: ApiService,
|
provide: ApiService,
|
||||||
useClass: useMocks ? MockApiService : LiveApiService,
|
useClass: useMocks ? MockApiService : LiveApiService,
|
||||||
},
|
},
|
||||||
{ provide: ErrorHandler, useClass: GlobalErrorHandler },
|
|
||||||
],
|
],
|
||||||
bootstrap: [AppComponent],
|
bootstrap: [AppComponent],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import { ErrorHandler, Injectable } from '@angular/core'
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class GlobalErrorHandler implements ErrorHandler {
|
|
||||||
|
|
||||||
handleError (e: any): void {
|
|
||||||
console.error(e)
|
|
||||||
const chunkFailedMessage = /Loading chunk [\d]+ failed/
|
|
||||||
|
|
||||||
if (chunkFailedMessage.test(e.message)) {
|
|
||||||
window.location.reload()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { Directive, ElementRef, Input } from '@angular/core'
|
||||||
|
import { AlertButton } from '@ionic/angular'
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: `button[alertButton], a[alertButton]`,
|
||||||
|
})
|
||||||
|
export class AlertButtonDirective implements AlertButton {
|
||||||
|
@Input()
|
||||||
|
icon?: string
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
role?: 'cancel' | 'destructive' | string
|
||||||
|
|
||||||
|
handler = () => {
|
||||||
|
this.elementRef.nativeElement.click()
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(private readonly elementRef: ElementRef<HTMLElement>) {}
|
||||||
|
|
||||||
|
get text(): string {
|
||||||
|
return this.elementRef.nativeElement.textContent?.trim() || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
get cssClass(): string[] {
|
||||||
|
return Array.from(this.elementRef.nativeElement.classList)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Directive, ElementRef, Input } from '@angular/core'
|
||||||
|
import { AlertInput } from '@ionic/angular'
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: `input[alertInput], textarea[alertInput]`,
|
||||||
|
})
|
||||||
|
export class AlertInputDirective<T> implements AlertInput {
|
||||||
|
@Input()
|
||||||
|
value?: T
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
label?: string
|
||||||
|
|
||||||
|
constructor(private readonly elementRef: ElementRef<HTMLInputElement>) {}
|
||||||
|
|
||||||
|
get checked(): boolean {
|
||||||
|
return this.elementRef.nativeElement.checked
|
||||||
|
}
|
||||||
|
|
||||||
|
get name(): string {
|
||||||
|
return this.elementRef.nativeElement.name
|
||||||
|
}
|
||||||
|
|
||||||
|
get type(): AlertInput['type'] {
|
||||||
|
return this.elementRef.nativeElement.type as AlertInput['type']
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import {
|
||||||
|
AfterViewInit,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
ContentChildren,
|
||||||
|
ElementRef,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
OnDestroy,
|
||||||
|
Output,
|
||||||
|
QueryList,
|
||||||
|
ViewChild,
|
||||||
|
} from '@angular/core'
|
||||||
|
import { AlertController, AlertOptions, IonicSafeString } from '@ionic/angular'
|
||||||
|
import { OverlayEventDetail } from '@ionic/core'
|
||||||
|
import { AlertButtonDirective } from './alert-button.directive'
|
||||||
|
import { AlertInputDirective } from './alert-input.directive'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'alert',
|
||||||
|
template: `
|
||||||
|
<div #message><ng-content></ng-content></div>
|
||||||
|
<ng-content select="[alertInput]"></ng-content>
|
||||||
|
<ng-content select="[alertButton]"></ng-content>
|
||||||
|
`,
|
||||||
|
styles: [':host { display: none !important; }'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class AlertComponent<T> implements AfterViewInit, OnDestroy {
|
||||||
|
@Output()
|
||||||
|
readonly dismiss = new EventEmitter<OverlayEventDetail<T>>()
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
header = ''
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
subHeader = ''
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
backdropDismiss = true
|
||||||
|
|
||||||
|
@ViewChild('message', { static: true })
|
||||||
|
private readonly content?: ElementRef<HTMLElement>
|
||||||
|
|
||||||
|
@ContentChildren(AlertButtonDirective)
|
||||||
|
private readonly buttons: QueryList<AlertButtonDirective> = new QueryList()
|
||||||
|
|
||||||
|
@ContentChildren(AlertInputDirective)
|
||||||
|
private readonly inputs: QueryList<AlertInputDirective<any>> = new QueryList()
|
||||||
|
|
||||||
|
private alert?: HTMLIonAlertElement
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly elementRef: ElementRef<HTMLElement>,
|
||||||
|
private readonly controller: AlertController,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
get cssClass(): string[] {
|
||||||
|
return Array.from(this.elementRef.nativeElement.classList)
|
||||||
|
}
|
||||||
|
|
||||||
|
get message(): IonicSafeString {
|
||||||
|
return new IonicSafeString(this.content?.nativeElement.innerHTML || '')
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngAfterViewInit() {
|
||||||
|
this.alert = await this.controller.create(this.getOptions())
|
||||||
|
this.alert.onDidDismiss().then(event => {
|
||||||
|
this.dismiss.emit(event)
|
||||||
|
})
|
||||||
|
|
||||||
|
await this.alert.present()
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnDestroy() {
|
||||||
|
await this.alert?.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
private getOptions(): AlertOptions {
|
||||||
|
const {
|
||||||
|
header,
|
||||||
|
subHeader,
|
||||||
|
message,
|
||||||
|
cssClass,
|
||||||
|
buttons,
|
||||||
|
inputs,
|
||||||
|
backdropDismiss,
|
||||||
|
} = this
|
||||||
|
return {
|
||||||
|
header,
|
||||||
|
subHeader,
|
||||||
|
message,
|
||||||
|
cssClass,
|
||||||
|
backdropDismiss,
|
||||||
|
buttons: buttons.toArray(),
|
||||||
|
inputs: inputs.toArray(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { NgModule } from '@angular/core'
|
||||||
|
import { AlertComponent } from './alert.component'
|
||||||
|
import { AlertButtonDirective } from './alert-button.directive'
|
||||||
|
import { AlertInputDirective } from './alert-input.directive'
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [AlertComponent, AlertButtonDirective, AlertInputDirective],
|
||||||
|
exports: [AlertComponent, AlertButtonDirective, AlertInputDirective],
|
||||||
|
})
|
||||||
|
export class AlertModule {}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { Directive, ElementRef, Input } from '@angular/core'
|
||||||
|
import { ToastButton } from '@ionic/angular'
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: `button[toastButton], a[toastButton]`,
|
||||||
|
})
|
||||||
|
export class ToastButtonDirective implements ToastButton {
|
||||||
|
@Input()
|
||||||
|
icon?: string
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
side?: 'start' | 'end'
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
role?: 'cancel' | string
|
||||||
|
|
||||||
|
handler = () => {
|
||||||
|
this.elementRef.nativeElement.click()
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(private readonly elementRef: ElementRef<HTMLElement>) {}
|
||||||
|
|
||||||
|
get text(): string | undefined {
|
||||||
|
return this.elementRef.nativeElement.textContent?.trim() || undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
get cssClass(): string[] {
|
||||||
|
return Array.from(this.elementRef.nativeElement.classList)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import {
|
||||||
|
AfterViewInit,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
ContentChildren,
|
||||||
|
ElementRef,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
OnDestroy,
|
||||||
|
Output,
|
||||||
|
QueryList,
|
||||||
|
ViewChild,
|
||||||
|
} from '@angular/core'
|
||||||
|
import { IonicSafeString, ToastController, ToastOptions } from '@ionic/angular'
|
||||||
|
import { OverlayEventDetail } from '@ionic/core'
|
||||||
|
import { ToastButtonDirective } from './toast-button.directive'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'toast',
|
||||||
|
template: `
|
||||||
|
<div #message><ng-content></ng-content></div>
|
||||||
|
<ng-content select="[toastButton]"></ng-content>
|
||||||
|
`,
|
||||||
|
styles: [':host { display: none !important; }'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class ToastComponent<T> implements AfterViewInit, OnDestroy {
|
||||||
|
@Output()
|
||||||
|
readonly dismiss = new EventEmitter<OverlayEventDetail<T>>()
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
header = ''
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
duration = 0
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
position: 'top' | 'bottom' | 'middle' = 'bottom'
|
||||||
|
|
||||||
|
@ViewChild('message', { static: true })
|
||||||
|
private readonly content?: ElementRef<HTMLElement>
|
||||||
|
|
||||||
|
@ContentChildren(ToastButtonDirective)
|
||||||
|
private readonly buttons: QueryList<ToastButtonDirective> = new QueryList()
|
||||||
|
|
||||||
|
private toast?: HTMLIonToastElement
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly elementRef: ElementRef<HTMLElement>,
|
||||||
|
private readonly controller: ToastController,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
get cssClass(): string[] {
|
||||||
|
return Array.from(this.elementRef.nativeElement.classList)
|
||||||
|
}
|
||||||
|
|
||||||
|
get message(): IonicSafeString {
|
||||||
|
return new IonicSafeString(this.content?.nativeElement.innerHTML || '')
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngAfterViewInit() {
|
||||||
|
this.toast = await this.controller.create(this.getOptions())
|
||||||
|
this.toast.onDidDismiss().then(event => {
|
||||||
|
this.dismiss.emit(event)
|
||||||
|
})
|
||||||
|
|
||||||
|
await this.toast.present()
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnDestroy() {
|
||||||
|
await this.toast?.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
private getOptions(): ToastOptions {
|
||||||
|
const { header, message, duration, position, cssClass, buttons } = this
|
||||||
|
return {
|
||||||
|
header,
|
||||||
|
message,
|
||||||
|
duration,
|
||||||
|
position,
|
||||||
|
cssClass,
|
||||||
|
buttons: buttons.toArray(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { NgModule } from '@angular/core'
|
||||||
|
import { ToastComponent } from './toast.component'
|
||||||
|
import { ToastButtonDirective } from './toast-button.directive'
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [ToastComponent, ToastButtonDirective],
|
||||||
|
exports: [ToastComponent, ToastButtonDirective],
|
||||||
|
})
|
||||||
|
export class ToastModule {}
|
||||||
@@ -5,10 +5,17 @@
|
|||||||
export * from './classes/http-error'
|
export * from './classes/http-error'
|
||||||
export * from './classes/rpc-error'
|
export * from './classes/rpc-error'
|
||||||
|
|
||||||
|
export * from './components/alert/alert.component'
|
||||||
|
export * from './components/alert/alert.module'
|
||||||
|
export * from './components/alert/alert-button.directive'
|
||||||
|
export * from './components/alert/alert-input.directive'
|
||||||
export * from './components/markdown/markdown.component'
|
export * from './components/markdown/markdown.component'
|
||||||
export * from './components/markdown/markdown.component.module'
|
export * from './components/markdown/markdown.component.module'
|
||||||
export * from './components/text-spinner/text-spinner.component'
|
export * from './components/text-spinner/text-spinner.component'
|
||||||
export * from './components/text-spinner/text-spinner.component.module'
|
export * from './components/text-spinner/text-spinner.component.module'
|
||||||
|
export * from './components/toast/toast.component'
|
||||||
|
export * from './components/toast/toast.module'
|
||||||
|
export * from './components/toast/toast-button.directive'
|
||||||
|
|
||||||
export * from './directives/element/element.directive'
|
export * from './directives/element/element.directive'
|
||||||
export * from './directives/element/element.module'
|
export * from './directives/element/element.module'
|
||||||
|
|||||||
@@ -4,11 +4,6 @@ export type WorkspaceConfig = {
|
|||||||
useMocks: boolean
|
useMocks: boolean
|
||||||
// each key corresponds to a project and values adjust settings for that project, eg: ui, setup-wizard, diagnostic-ui
|
// each key corresponds to a project and values adjust settings for that project, eg: ui, setup-wizard, diagnostic-ui
|
||||||
ui: {
|
ui: {
|
||||||
patchDb: {
|
|
||||||
poll: {
|
|
||||||
cooldown: number /* in ms */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
api: {
|
api: {
|
||||||
url: string
|
url: string
|
||||||
version: string
|
version: string
|
||||||
|
|||||||
@@ -18,4 +18,5 @@
|
|||||||
<ion-footer>
|
<ion-footer>
|
||||||
<footer appFooter></footer>
|
<footer appFooter></footer>
|
||||||
</ion-footer>
|
</ion-footer>
|
||||||
|
<toast-container></toast-container>
|
||||||
</ion-app>
|
</ion-app>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Component, Inject, OnDestroy } from '@angular/core'
|
import { Component, OnDestroy } from '@angular/core'
|
||||||
|
import { merge } from 'rxjs'
|
||||||
import { AuthService } from './services/auth.service'
|
import { AuthService } from './services/auth.service'
|
||||||
import { SplitPaneTracker } from './services/split-pane.service'
|
import { SplitPaneTracker } from './services/split-pane.service'
|
||||||
import { merge, Observable } from 'rxjs'
|
import { PatchDataService } from './services/patch-data.service'
|
||||||
import { GLOBAL_SERVICE } from './app/global/global.module'
|
import { PatchMonitorService } from './services/patch-monitor.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
@@ -10,13 +11,13 @@ import { GLOBAL_SERVICE } from './app/global/global.module'
|
|||||||
styleUrls: ['app.component.scss'],
|
styleUrls: ['app.component.scss'],
|
||||||
})
|
})
|
||||||
export class AppComponent implements OnDestroy {
|
export class AppComponent implements OnDestroy {
|
||||||
readonly subscription = merge(...this.services).subscribe()
|
readonly subscription = merge(this.patchData, this.patchMonitor).subscribe()
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(GLOBAL_SERVICE)
|
private readonly patchData: PatchDataService,
|
||||||
private readonly services: readonly Observable<unknown>[],
|
private readonly patchMonitor: PatchMonitorService,
|
||||||
readonly authService: AuthService,
|
|
||||||
private readonly splitPane: SplitPaneTracker,
|
private readonly splitPane: SplitPaneTracker,
|
||||||
|
readonly authService: AuthService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
splitPaneVisible({ detail }: any) {
|
splitPaneVisible({ detail }: any) {
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ import { FooterModule } from './app/footer/footer.module'
|
|||||||
import { MenuModule } from './app/menu/menu.module'
|
import { MenuModule } from './app/menu/menu.module'
|
||||||
import { EnterModule } from './app/enter/enter.module'
|
import { EnterModule } from './app/enter/enter.module'
|
||||||
import { APP_PROVIDERS } from './app.providers'
|
import { APP_PROVIDERS } from './app.providers'
|
||||||
import { GlobalModule } from './app/global/global.module'
|
|
||||||
import { PatchDbModule } from './services/patch-db/patch-db.module'
|
import { PatchDbModule } from './services/patch-db/patch-db.module'
|
||||||
|
import { ToastContainerModule } from './components/toast-container/toast-container.module'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [AppComponent],
|
declarations: [AppComponent],
|
||||||
@@ -45,8 +45,8 @@ import { PatchDbModule } from './services/patch-db/patch-db.module'
|
|||||||
MonacoEditorModule,
|
MonacoEditorModule,
|
||||||
SharedPipesModule,
|
SharedPipesModule,
|
||||||
MarketplaceModule,
|
MarketplaceModule,
|
||||||
GlobalModule,
|
|
||||||
PatchDbModule,
|
PatchDbModule,
|
||||||
|
ToastContainerModule,
|
||||||
],
|
],
|
||||||
providers: APP_PROVIDERS,
|
providers: APP_PROVIDERS,
|
||||||
bootstrap: [AppComponent],
|
bootstrap: [AppComponent],
|
||||||
|
|||||||
@@ -5,12 +5,10 @@ import { Router, RouteReuseStrategy } from '@angular/router'
|
|||||||
import { IonicRouteStrategy, IonNav } from '@ionic/angular'
|
import { IonicRouteStrategy, IonNav } from '@ionic/angular'
|
||||||
import { Storage } from '@ionic/storage-angular'
|
import { Storage } from '@ionic/storage-angular'
|
||||||
import { WorkspaceConfig } from '@start9labs/shared'
|
import { WorkspaceConfig } from '@start9labs/shared'
|
||||||
|
|
||||||
import { ApiService } from './services/api/embassy-api.service'
|
import { ApiService } from './services/api/embassy-api.service'
|
||||||
import { MockApiService } from './services/api/embassy-mock-api.service'
|
import { MockApiService } from './services/api/embassy-mock-api.service'
|
||||||
import { LiveApiService } from './services/api/embassy-live-api.service'
|
import { LiveApiService } from './services/api/embassy-live-api.service'
|
||||||
import { BOOTSTRAPPER, PATCH_CACHE } from './services/patch-db/patch-db.factory'
|
import { BOOTSTRAPPER, PATCH_CACHE } from './services/patch-db/patch-db.factory'
|
||||||
import { GlobalErrorHandler } from './services/global-error-handler.service'
|
|
||||||
import { AuthService } from './services/auth.service'
|
import { AuthService } from './services/auth.service'
|
||||||
import { LocalStorageService } from './services/local-storage.service'
|
import { LocalStorageService } from './services/local-storage.service'
|
||||||
import { DataModel } from './services/patch-db/data-model'
|
import { DataModel } from './services/patch-db/data-model'
|
||||||
@@ -30,10 +28,6 @@ export const APP_PROVIDERS: Provider[] = [
|
|||||||
provide: ApiService,
|
provide: ApiService,
|
||||||
useClass: useMocks ? MockApiService : LiveApiService,
|
useClass: useMocks ? MockApiService : LiveApiService,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
provide: ErrorHandler,
|
|
||||||
useClass: GlobalErrorHandler,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
provide: APP_INITIALIZER,
|
provide: APP_INITIALIZER,
|
||||||
deps: [
|
deps: [
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||||
|
|
||||||
import { heightCollapse } from '../../util/animations'
|
import { heightCollapse } from '../../util/animations'
|
||||||
import { PatchDbService } from '../../services/patch-db/patch-db.service'
|
import { PatchDbService } from '../../services/patch-db/patch-db.service'
|
||||||
import { map } from 'rxjs/operators'
|
import { map } from 'rxjs/operators'
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
import {
|
|
||||||
ClassProvider,
|
|
||||||
ExistingProvider,
|
|
||||||
Inject,
|
|
||||||
InjectionToken,
|
|
||||||
NgModule,
|
|
||||||
OnDestroy,
|
|
||||||
Type,
|
|
||||||
} from '@angular/core'
|
|
||||||
import { merge, Observable } from 'rxjs'
|
|
||||||
import { OfflineService } from './services/offline.service'
|
|
||||||
import { LogoutService } from './services/logout.service'
|
|
||||||
import { PatchMonitorService } from './services/patch-monitor.service'
|
|
||||||
import { PatchDataService } from './services/patch-data.service'
|
|
||||||
import { ConnectionMonitorService } from './services/connection-monitor.service'
|
|
||||||
import { UnreadToastService } from './services/unread-toast.service'
|
|
||||||
import { RefreshToastService } from './services/refresh-toast.service'
|
|
||||||
import { UpdateToastService } from './services/update-toast.service'
|
|
||||||
|
|
||||||
export const GLOBAL_SERVICE = new InjectionToken<
|
|
||||||
readonly Observable<unknown>[]
|
|
||||||
>('A multi token of global Observable services')
|
|
||||||
|
|
||||||
// This module is purely for providers organization purposes
|
|
||||||
@NgModule({
|
|
||||||
providers: [
|
|
||||||
[
|
|
||||||
ConnectionMonitorService,
|
|
||||||
LogoutService,
|
|
||||||
OfflineService,
|
|
||||||
RefreshToastService,
|
|
||||||
UnreadToastService,
|
|
||||||
UpdateToastService,
|
|
||||||
].map(useClass),
|
|
||||||
[PatchDataService, PatchMonitorService].map(useExisting),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class GlobalModule {}
|
|
||||||
|
|
||||||
function useClass(useClass: Type<unknown>): ClassProvider {
|
|
||||||
return {
|
|
||||||
provide: GLOBAL_SERVICE,
|
|
||||||
multi: true,
|
|
||||||
useClass,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function useExisting(useExisting: Type<unknown>): ExistingProvider {
|
|
||||||
return {
|
|
||||||
provide: GLOBAL_SERVICE,
|
|
||||||
multi: true,
|
|
||||||
useExisting,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core'
|
|
||||||
import { EMPTY, Observable } from 'rxjs'
|
|
||||||
import { switchMap } from 'rxjs/operators'
|
|
||||||
|
|
||||||
import { PatchMonitorService } from './patch-monitor.service'
|
|
||||||
import { ConnectionService } from 'src/app/services/connection.service'
|
|
||||||
|
|
||||||
// Start connection monitor upon PatchDb start
|
|
||||||
@Injectable()
|
|
||||||
export class ConnectionMonitorService extends Observable<unknown> {
|
|
||||||
private readonly stream$ = this.patchMonitor.pipe(
|
|
||||||
switchMap(started => (started ? this.connectionService.start() : EMPTY)),
|
|
||||||
)
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly patchMonitor: PatchMonitorService,
|
|
||||||
private readonly connectionService: ConnectionService,
|
|
||||||
) {
|
|
||||||
super(subscriber => this.stream$.subscribe(subscriber))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { Injectable, NgZone } from '@angular/core'
|
|
||||||
import { Router } from '@angular/router'
|
|
||||||
import { filter, tap } from 'rxjs/operators'
|
|
||||||
import { Observable } from 'rxjs'
|
|
||||||
|
|
||||||
import { AuthService } from 'src/app/services/auth.service'
|
|
||||||
|
|
||||||
// Redirect to login page upon broken authorization
|
|
||||||
@Injectable()
|
|
||||||
export class LogoutService extends Observable<unknown> {
|
|
||||||
private readonly stream$ = this.authService.isVerified$.pipe(
|
|
||||||
filter(verified => !verified),
|
|
||||||
tap(() => {
|
|
||||||
this.zone.run(() => {
|
|
||||||
this.router.navigate(['/login'], { replaceUrl: true })
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly authService: AuthService,
|
|
||||||
private readonly zone: NgZone,
|
|
||||||
private readonly router: Router,
|
|
||||||
) {
|
|
||||||
super(subscriber => this.stream$.subscribe(subscriber))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core'
|
|
||||||
import { ToastController, ToastOptions } from '@ionic/angular'
|
|
||||||
import { ToastButton } from '@ionic/core'
|
|
||||||
import { EMPTY, from, Observable } from 'rxjs'
|
|
||||||
import {
|
|
||||||
debounceTime,
|
|
||||||
distinctUntilChanged,
|
|
||||||
filter,
|
|
||||||
map,
|
|
||||||
switchMap,
|
|
||||||
tap,
|
|
||||||
} from 'rxjs/operators'
|
|
||||||
|
|
||||||
import { AuthService } from 'src/app/services/auth.service'
|
|
||||||
import {
|
|
||||||
ConnectionFailure,
|
|
||||||
ConnectionService,
|
|
||||||
} from 'src/app/services/connection.service'
|
|
||||||
|
|
||||||
// Watch for connection status
|
|
||||||
@Injectable()
|
|
||||||
export class OfflineService extends Observable<unknown> {
|
|
||||||
private current?: HTMLIonToastElement
|
|
||||||
|
|
||||||
private readonly connection$ = this.connectionService
|
|
||||||
.watchFailure$()
|
|
||||||
.pipe(distinctUntilChanged(), debounceTime(500))
|
|
||||||
|
|
||||||
private readonly stream$ = this.authService.isVerified$.pipe(
|
|
||||||
// Close on logout
|
|
||||||
tap(() => this.current?.dismiss()),
|
|
||||||
switchMap(verified => (verified ? this.connection$ : EMPTY)),
|
|
||||||
// Close on change to connection state
|
|
||||||
tap(() => this.current?.dismiss()),
|
|
||||||
filter(connection => connection !== ConnectionFailure.None),
|
|
||||||
map(getMessage),
|
|
||||||
switchMap(({ message, link }) =>
|
|
||||||
this.getToast().pipe(
|
|
||||||
tap(toast => {
|
|
||||||
this.current = toast
|
|
||||||
|
|
||||||
toast.message = message
|
|
||||||
toast.buttons = getButtons(link)
|
|
||||||
toast.present()
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly authService: AuthService,
|
|
||||||
private readonly connectionService: ConnectionService,
|
|
||||||
private readonly toastCtrl: ToastController,
|
|
||||||
) {
|
|
||||||
super(subscriber => this.stream$.subscribe(subscriber))
|
|
||||||
}
|
|
||||||
|
|
||||||
private getToast(): Observable<HTMLIonToastElement> {
|
|
||||||
return from(this.toastCtrl.create(TOAST))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const TOAST: ToastOptions = {
|
|
||||||
header: 'Unable to Connect',
|
|
||||||
cssClass: 'warning-toast',
|
|
||||||
message: '',
|
|
||||||
position: 'bottom',
|
|
||||||
duration: 0,
|
|
||||||
buttons: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMessage(failure: ConnectionFailure): OfflineMessage {
|
|
||||||
switch (failure) {
|
|
||||||
case ConnectionFailure.Network:
|
|
||||||
return { message: 'Phone or computer has no network connection.' }
|
|
||||||
case ConnectionFailure.Tor:
|
|
||||||
return {
|
|
||||||
message: 'Browser unable to connect over Tor.',
|
|
||||||
link: 'https://start9.com/latest/support/common-issues',
|
|
||||||
}
|
|
||||||
case ConnectionFailure.Lan:
|
|
||||||
return {
|
|
||||||
message: 'Embassy not found on Local Area Network.',
|
|
||||||
link: 'https://start9.com/latest/support/common-issues',
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return { message: '' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getButtons(link?: string): ToastButton[] {
|
|
||||||
const buttons: ToastButton[] = [
|
|
||||||
{
|
|
||||||
side: 'start',
|
|
||||||
icon: 'close',
|
|
||||||
handler: () => true,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
if (link) {
|
|
||||||
buttons.push({
|
|
||||||
side: 'end',
|
|
||||||
text: 'View solutions',
|
|
||||||
handler: () => {
|
|
||||||
window.open(link, '_blank', 'noreferrer')
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return buttons
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OfflineMessage {
|
|
||||||
readonly message: string
|
|
||||||
readonly link?: string
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core'
|
|
||||||
import { AlertController, AlertOptions } from '@ionic/angular'
|
|
||||||
import { EMPTY, from, Observable } from 'rxjs'
|
|
||||||
import { filter, switchMap } from 'rxjs/operators'
|
|
||||||
import { Emver } from '@start9labs/shared'
|
|
||||||
|
|
||||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
|
||||||
import { PatchDataService } from './patch-data.service'
|
|
||||||
|
|
||||||
// Watch version to refresh browser window
|
|
||||||
@Injectable()
|
|
||||||
export class RefreshToastService extends Observable<unknown> {
|
|
||||||
private readonly stream$ = this.patchData.pipe(
|
|
||||||
switchMap(data =>
|
|
||||||
data ? this.patch.watch$('server-info', 'version') : EMPTY,
|
|
||||||
),
|
|
||||||
filter(version => !!this.emver.compare(this.config.version, version)),
|
|
||||||
switchMap(() => this.getAlert()),
|
|
||||||
switchMap(alert => alert.present()),
|
|
||||||
)
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly patchData: PatchDataService,
|
|
||||||
private readonly patch: PatchDbService,
|
|
||||||
private readonly emver: Emver,
|
|
||||||
private readonly config: ConfigService,
|
|
||||||
private readonly alertCtrl: AlertController,
|
|
||||||
) {
|
|
||||||
super(subscriber => this.stream$.subscribe(subscriber))
|
|
||||||
}
|
|
||||||
|
|
||||||
private getAlert(): Observable<HTMLIonAlertElement> {
|
|
||||||
return from(this.alertCtrl.create(ALERT))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ALERT: AlertOptions = {
|
|
||||||
backdropDismiss: true,
|
|
||||||
header: 'Refresh Needed',
|
|
||||||
message:
|
|
||||||
'Your user interface is cached and out of date. Hard refresh the page to get the latest UI.',
|
|
||||||
buttons: [
|
|
||||||
{
|
|
||||||
text: 'Refresh Page',
|
|
||||||
cssClass: 'enter-click',
|
|
||||||
handler: () => {
|
|
||||||
location.reload()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core'
|
|
||||||
import { Router } from '@angular/router'
|
|
||||||
import { ToastController, ToastOptions } from '@ionic/angular'
|
|
||||||
import { EMPTY, Observable, ObservableInput } from 'rxjs'
|
|
||||||
import { filter, pairwise, switchMap, tap } from 'rxjs/operators'
|
|
||||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
|
||||||
import { PatchDataService } from './patch-data.service'
|
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
|
||||||
|
|
||||||
// Watch unread notification count to display toast
|
|
||||||
@Injectable()
|
|
||||||
export class UnreadToastService extends Observable<unknown> {
|
|
||||||
private unreadToast?: HTMLIonToastElement
|
|
||||||
|
|
||||||
private readonly stream$ = this.patchData.pipe(
|
|
||||||
switchMap<DataModel | null, ObservableInput<number>>(data => {
|
|
||||||
if (data) {
|
|
||||||
return this.patch.watch$('server-info', 'unread-notification-count')
|
|
||||||
}
|
|
||||||
|
|
||||||
this.unreadToast?.dismiss()
|
|
||||||
|
|
||||||
return EMPTY
|
|
||||||
}),
|
|
||||||
pairwise(),
|
|
||||||
filter(([prev, cur]) => cur > prev),
|
|
||||||
tap(() => {
|
|
||||||
this.showToast()
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
TOAST: ToastOptions = {
|
|
||||||
header: 'Embassy',
|
|
||||||
message: `New notifications`,
|
|
||||||
position: 'bottom',
|
|
||||||
duration: 4000,
|
|
||||||
buttons: [
|
|
||||||
{
|
|
||||||
side: 'start',
|
|
||||||
icon: 'close',
|
|
||||||
handler: () => true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
side: 'end',
|
|
||||||
text: 'View',
|
|
||||||
handler: () => {
|
|
||||||
this.router.navigate(['/notifications'], {
|
|
||||||
queryParams: { toast: true },
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly router: Router,
|
|
||||||
private readonly patchData: PatchDataService,
|
|
||||||
private readonly patch: PatchDbService,
|
|
||||||
private readonly toastCtrl: ToastController,
|
|
||||||
) {
|
|
||||||
super(subscriber => this.stream$.subscribe(subscriber))
|
|
||||||
}
|
|
||||||
|
|
||||||
private async showToast() {
|
|
||||||
await this.unreadToast?.dismiss()
|
|
||||||
|
|
||||||
this.unreadToast = await this.toastCtrl.create(this.TOAST)
|
|
||||||
this.unreadToast.buttons?.push()
|
|
||||||
|
|
||||||
await this.unreadToast.present()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core'
|
|
||||||
import {
|
|
||||||
LoadingController,
|
|
||||||
LoadingOptions,
|
|
||||||
ToastController,
|
|
||||||
ToastOptions,
|
|
||||||
} from '@ionic/angular'
|
|
||||||
import { EMPTY, Observable } from 'rxjs'
|
|
||||||
import { distinctUntilChanged, filter, switchMap } from 'rxjs/operators'
|
|
||||||
import { ErrorToastService } from '@start9labs/shared'
|
|
||||||
|
|
||||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
|
||||||
import { PatchDataService } from './patch-data.service'
|
|
||||||
|
|
||||||
// Watch status to present toast for updated state
|
|
||||||
@Injectable()
|
|
||||||
export class UpdateToastService extends Observable<unknown> {
|
|
||||||
private updateToast?: HTMLIonToastElement
|
|
||||||
|
|
||||||
private readonly stream$ = this.patchData.pipe(
|
|
||||||
switchMap(data => {
|
|
||||||
if (data) {
|
|
||||||
return this.patch.watch$('server-info', 'status-info', 'updated')
|
|
||||||
}
|
|
||||||
|
|
||||||
this.errToast.dismiss()
|
|
||||||
this.updateToast?.dismiss()
|
|
||||||
|
|
||||||
return EMPTY
|
|
||||||
}),
|
|
||||||
distinctUntilChanged(),
|
|
||||||
filter(Boolean),
|
|
||||||
switchMap(() => this.showToast()),
|
|
||||||
)
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly patchData: PatchDataService,
|
|
||||||
private readonly patch: PatchDbService,
|
|
||||||
private readonly embassyApi: ApiService,
|
|
||||||
private readonly toastCtrl: ToastController,
|
|
||||||
private readonly errToast: ErrorToastService,
|
|
||||||
private readonly loadingCtrl: LoadingController,
|
|
||||||
) {
|
|
||||||
super(subscriber => this.stream$.subscribe(subscriber))
|
|
||||||
}
|
|
||||||
|
|
||||||
LOADER: LoadingOptions = {
|
|
||||||
message: 'Restarting...',
|
|
||||||
}
|
|
||||||
|
|
||||||
TOAST: ToastOptions = {
|
|
||||||
header: 'EOS download complete!',
|
|
||||||
message:
|
|
||||||
'Restart your Embassy for these updates to take effect. It can take several minutes to come back online.',
|
|
||||||
position: 'bottom',
|
|
||||||
duration: 0,
|
|
||||||
cssClass: 'success-toast',
|
|
||||||
buttons: [
|
|
||||||
{
|
|
||||||
side: 'start',
|
|
||||||
icon: 'close',
|
|
||||||
handler: () => true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
side: 'end',
|
|
||||||
text: 'Restart',
|
|
||||||
handler: () => {
|
|
||||||
this.restart()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
private async showToast() {
|
|
||||||
await this.updateToast?.dismiss()
|
|
||||||
|
|
||||||
this.updateToast = await this.toastCtrl.create(this.TOAST)
|
|
||||||
|
|
||||||
await this.updateToast.present()
|
|
||||||
}
|
|
||||||
|
|
||||||
private async restart(): Promise<void> {
|
|
||||||
const loader = await this.loadingCtrl.create(this.LOADER)
|
|
||||||
|
|
||||||
await loader.present()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.embassyApi.restartServer({})
|
|
||||||
} catch (e: any) {
|
|
||||||
await this.errToast.present(e)
|
|
||||||
} finally {
|
|
||||||
await loader.dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -94,7 +94,7 @@ export class MenuComponent {
|
|||||||
|
|
||||||
// should wipe cache independent of actual BE logout
|
// should wipe cache independent of actual BE logout
|
||||||
private logout() {
|
private logout() {
|
||||||
this.embassyApi.logout({})
|
this.embassyApi.logout({}).catch(e => console.error('Failed to log out', e))
|
||||||
this.authService.setUnverified()
|
this.authService.setUnverified()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<ion-badge
|
<ion-badge
|
||||||
*ngIf="unreadCount && !sidebarOpen"
|
*ngIf="!(sidebarOpen$ | async) && (unreadCount$ | async) as unreadCount"
|
||||||
mode="md"
|
mode="md"
|
||||||
class="md-badge"
|
class="md-badge"
|
||||||
color="danger"
|
color="danger"
|
||||||
|
|||||||
@@ -1,37 +1,19 @@
|
|||||||
import { Component } from '@angular/core'
|
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||||
import { SplitPaneTracker } from 'src/app/services/split-pane.service'
|
import { SplitPaneTracker } from 'src/app/services/split-pane.service'
|
||||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||||
import { combineLatest, Subscription } from 'rxjs'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'badge-menu-button',
|
selector: 'badge-menu-button',
|
||||||
templateUrl: './badge-menu.component.html',
|
templateUrl: './badge-menu.component.html',
|
||||||
styleUrls: ['./badge-menu.component.scss'],
|
styleUrls: ['./badge-menu.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class BadgeMenuComponent {
|
export class BadgeMenuComponent {
|
||||||
unreadCount = 0
|
unreadCount$ = this.patch.watch$('server-info', 'unread-notification-count')
|
||||||
sidebarOpen = false
|
sidebarOpen$ = this.splitPane.sidebarOpen$
|
||||||
|
|
||||||
subs: Subscription[] = []
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly splitPane: SplitPaneTracker,
|
private readonly splitPane: SplitPaneTracker,
|
||||||
private readonly patch: PatchDbService,
|
private readonly patch: PatchDbService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.subs = [
|
|
||||||
combineLatest([
|
|
||||||
this.patch.watch$('server-info', 'unread-notification-count'),
|
|
||||||
this.splitPane.sidebarOpen$,
|
|
||||||
]).subscribe(([unread, menu]) => {
|
|
||||||
this.unreadCount = unread
|
|
||||||
this.sidebarOpen = menu
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy() {
|
|
||||||
this.subs.forEach(sub => sub.unsubscribe())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
id="scroller"
|
id="scroller"
|
||||||
*ngIf="!loading && needInfinite"
|
*ngIf="!loading && needInfinite"
|
||||||
position="top"
|
position="top"
|
||||||
threshold="0"
|
threshold="1000"
|
||||||
(ionInfinite)="doInfinite($event)"
|
(ionInfinite)="doInfinite($event)"
|
||||||
>
|
>
|
||||||
<ion-infinite-scroll-content
|
<ion-infinite-scroll-content
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { DOCUMENT } from '@angular/common'
|
import { Component, Input, ViewChild } from '@angular/core'
|
||||||
import { Component, Inject, Input, ViewChild } from '@angular/core'
|
|
||||||
import { IonContent, LoadingController } from '@ionic/angular'
|
import { IonContent, LoadingController } from '@ionic/angular'
|
||||||
import { map, takeUntil, timer } from 'rxjs'
|
import { map, takeUntil, timer } from 'rxjs'
|
||||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||||
@@ -48,11 +47,10 @@ export class LogsComponent {
|
|||||||
isOnBottom = true
|
isOnBottom = true
|
||||||
autoScroll = true
|
autoScroll = true
|
||||||
websocketFail = false
|
websocketFail = false
|
||||||
limit = 200
|
limit = 400
|
||||||
toProcess: Log[] = []
|
toProcess: Log[] = []
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DOCUMENT) private readonly document: Document,
|
|
||||||
private readonly errToast: ErrorToastService,
|
private readonly errToast: ErrorToastService,
|
||||||
private readonly destroy$: DestroyService,
|
private readonly destroy$: DestroyService,
|
||||||
private readonly api: ApiService,
|
private readonly api: ApiService,
|
||||||
@@ -63,17 +61,13 @@ export class LogsComponent {
|
|||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
try {
|
try {
|
||||||
const { 'start-cursor': startCursor, guid } = await this.followLogs({
|
const { 'start-cursor': startCursor, guid } = await this.followLogs({
|
||||||
limit: 100,
|
limit: this.limit,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.startCursor = startCursor
|
this.startCursor = startCursor
|
||||||
|
|
||||||
const host = this.document.location.host
|
|
||||||
const protocol =
|
|
||||||
this.document.location.protocol === 'http:' ? 'ws' : 'wss'
|
|
||||||
|
|
||||||
const config: WebSocketSubjectConfig<Log> = {
|
const config: WebSocketSubjectConfig<Log> = {
|
||||||
url: `${protocol}://${host}/ws/rpc/${guid}`,
|
url: `/rpc/${guid}`,
|
||||||
openObserver: {
|
openObserver: {
|
||||||
next: () => {
|
next: () => {
|
||||||
console.log('**** LOGS WEBSOCKET OPEN ****')
|
console.log('**** LOGS WEBSOCKET OPEN ****')
|
||||||
@@ -159,7 +153,7 @@ export class LogsComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private processJob() {
|
private processJob() {
|
||||||
timer(0, 500)
|
timer(100, 500)
|
||||||
.pipe(
|
.pipe(
|
||||||
map((_, index) => index),
|
map((_, index) => index),
|
||||||
takeUntil(this.destroy$),
|
takeUntil(this.destroy$),
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
<p
|
<p
|
||||||
[style.color]="
|
[style.color]="
|
||||||
(disconnected$ | async)
|
(connected$ | async) ? 'var(--ion-color-' + rendering.color + ')' : 'gray'
|
||||||
? 'gray'
|
|
||||||
: 'var(--ion-color-' + rendering.color + ')'
|
|
||||||
"
|
"
|
||||||
[style.font-size]="size"
|
[style.font-size]="size"
|
||||||
[style.font-style]="style"
|
[style.font-style]="style"
|
||||||
[style.font-weight]="weight"
|
[style.font-weight]="weight"
|
||||||
>
|
>
|
||||||
<span *ngIf="!installProgress">
|
<span *ngIf="!installProgress">
|
||||||
{{ (disconnected$ | async) ? 'Unknown' : rendering.display }}
|
{{ (connected$ | async) ? rendering.display : 'Unknown' }}
|
||||||
<span *ngIf="rendering.showDots" class="loading-dots"></span>
|
<span *ngIf="rendering.showDots" class="loading-dots"></span>
|
||||||
<span
|
<span
|
||||||
*ngIf="
|
*ngIf="
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export class StatusComponent {
|
|||||||
@Input() installProgress?: InstallProgress
|
@Input() installProgress?: InstallProgress
|
||||||
@Input() sigtermTimeout?: string | null = null
|
@Input() sigtermTimeout?: string | null = null
|
||||||
|
|
||||||
disconnected$ = this.connectionService.watchDisconnected$()
|
readonly connected$ = this.connectionService.connected$
|
||||||
|
|
||||||
constructor(private readonly connectionService: ConnectionService) {}
|
constructor(private readonly connectionService: ConnectionService) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<toast
|
||||||
|
*ngIf="visible$ | async as message"
|
||||||
|
header="Embassy"
|
||||||
|
[duration]="4000"
|
||||||
|
(dismiss)="onDismiss()"
|
||||||
|
>
|
||||||
|
New notifications
|
||||||
|
<button toastButton icon="close" side="start" (click)="onDismiss()"></button>
|
||||||
|
<a
|
||||||
|
toastButton
|
||||||
|
side="end"
|
||||||
|
routerLink="/notifications"
|
||||||
|
[queryParams]="{ toast: true }"
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</a>
|
||||||
|
</toast>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
|
||||||
|
import { Observable, Subject, merge } from 'rxjs'
|
||||||
|
|
||||||
|
import { NotificationsToastService } from './notifications-toast.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'notifications-toast',
|
||||||
|
templateUrl: './notifications-toast.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class NotificationsToastComponent {
|
||||||
|
private readonly dismiss$ = new Subject<boolean>()
|
||||||
|
|
||||||
|
readonly visible$: Observable<boolean> = merge(
|
||||||
|
this.dismiss$,
|
||||||
|
this.notifications$,
|
||||||
|
)
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(NotificationsToastService)
|
||||||
|
private readonly notifications$: Observable<boolean>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
onDismiss() {
|
||||||
|
this.dismiss$.next(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
import { endWith, Observable } from 'rxjs'
|
||||||
|
import { filter, map, pairwise } from 'rxjs/operators'
|
||||||
|
import { exists } from '@start9labs/shared'
|
||||||
|
import { PatchDbService } from '../../../services/patch-db/patch-db.service'
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class NotificationsToastService extends Observable<boolean> {
|
||||||
|
private readonly stream$ = this.patch
|
||||||
|
.watch$('server-info', 'unread-notification-count')
|
||||||
|
.pipe(
|
||||||
|
filter(exists),
|
||||||
|
pairwise(),
|
||||||
|
map(([prev, cur]) => cur > prev),
|
||||||
|
endWith(false),
|
||||||
|
)
|
||||||
|
|
||||||
|
constructor(private readonly patch: PatchDbService) {
|
||||||
|
super(subscriber => this.stream$.subscribe(subscriber))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<toast
|
||||||
|
*ngIf="content$ | async as content"
|
||||||
|
class="warning-toast"
|
||||||
|
header="Unable to Connect"
|
||||||
|
(dismiss)="onDismiss()"
|
||||||
|
>
|
||||||
|
{{ content.message }}
|
||||||
|
<button toastButton icon="close" side="start" (click)="onDismiss()"></button>
|
||||||
|
<a
|
||||||
|
*ngIf="content.link"
|
||||||
|
toastButton
|
||||||
|
side="end"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
[href]="content.link"
|
||||||
|
>
|
||||||
|
View solutions
|
||||||
|
</a>
|
||||||
|
</toast>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
|
||||||
|
import { Observable, Subject, merge, tap, map } from 'rxjs'
|
||||||
|
|
||||||
|
import { OfflineMessage, OfflineToastService } from './offline-toast.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'offline-toast',
|
||||||
|
templateUrl: './offline-toast.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class OfflineToastComponent {
|
||||||
|
private readonly dismiss$ = new Subject<null>()
|
||||||
|
|
||||||
|
readonly content$ = merge(this.dismiss$, this.failure$)
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(OfflineToastService)
|
||||||
|
private readonly failure$: Observable<OfflineMessage | null>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
onDismiss() {
|
||||||
|
this.dismiss$.next(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
import { combineLatest, Observable, of } from 'rxjs'
|
||||||
|
import { map, switchMap } from 'rxjs/operators'
|
||||||
|
import { AuthService } from 'src/app/services/auth.service'
|
||||||
|
import { ConnectionService } from 'src/app/services/connection.service'
|
||||||
|
|
||||||
|
export interface OfflineMessage {
|
||||||
|
readonly message: string
|
||||||
|
readonly link?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for connection status
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class OfflineToastService extends Observable<OfflineMessage | null> {
|
||||||
|
private readonly stream$ = this.authService.isVerified$.pipe(
|
||||||
|
switchMap(verified => (verified ? this.failure$ : of(null))),
|
||||||
|
)
|
||||||
|
|
||||||
|
private readonly failure$ = combineLatest([
|
||||||
|
this.connectionService.networkConnected$,
|
||||||
|
this.connectionService.websocketConnected$,
|
||||||
|
]).pipe(
|
||||||
|
map(([network, websocket]) => {
|
||||||
|
if (!network) return { message: 'No Internet' }
|
||||||
|
if (!websocket)
|
||||||
|
return {
|
||||||
|
message: 'Connecting to Embassy...',
|
||||||
|
link: 'https://start9.com/latest/support/common-issues',
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly authService: AuthService,
|
||||||
|
private readonly connectionService: ConnectionService,
|
||||||
|
) {
|
||||||
|
super(subscriber => this.stream$.subscribe(subscriber))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<alert *ngIf="show$ | async" header="Refresh needed" (dismiss)="onDismiss()">
|
||||||
|
Your user interface is cached and out of date. Hard refresh the page to get
|
||||||
|
the latest UI.
|
||||||
|
<a alertButton class="enter-click" href=".">Refresh Page</a>
|
||||||
|
</alert>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
|
||||||
|
import { Observable, Subject, merge } from 'rxjs'
|
||||||
|
|
||||||
|
import { RefreshAlertService } from './refresh-alert.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'refresh-alert',
|
||||||
|
templateUrl: './refresh-alert.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class RefreshAlertComponent {
|
||||||
|
private readonly dismiss$ = new Subject<boolean>()
|
||||||
|
|
||||||
|
readonly show$ = merge(this.dismiss$, this.refresh$)
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(RefreshAlertService) private readonly refresh$: Observable<boolean>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
onDismiss() {
|
||||||
|
this.dismiss$.next(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
import { endWith, Observable } from 'rxjs'
|
||||||
|
import { map } from 'rxjs/operators'
|
||||||
|
import { Emver } from '@start9labs/shared'
|
||||||
|
|
||||||
|
import { PatchDbService } from '../../../services/patch-db/patch-db.service'
|
||||||
|
import { ConfigService } from '../../../services/config.service'
|
||||||
|
|
||||||
|
// Watch for connection status
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class RefreshAlertService extends Observable<boolean> {
|
||||||
|
private readonly stream$ = this.patch.watch$('server-info', 'version').pipe(
|
||||||
|
map(version => !!this.emver.compare(this.config.version, version)),
|
||||||
|
endWith(false),
|
||||||
|
)
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly patch: PatchDbService,
|
||||||
|
private readonly emver: Emver,
|
||||||
|
private readonly config: ConfigService,
|
||||||
|
) {
|
||||||
|
super(subscriber => this.stream$.subscribe(subscriber))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<notifications-toast></notifications-toast>
|
||||||
|
<offline-toast></offline-toast>
|
||||||
|
<refresh-alert></refresh-alert>
|
||||||
|
<update-toast></update-toast>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'toast-container',
|
||||||
|
templateUrl: './toast-container.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class ToastContainerComponent {}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { NgModule } from '@angular/core'
|
||||||
|
import { RouterModule } from '@angular/router'
|
||||||
|
import { AlertModule, ToastModule } from '@start9labs/shared'
|
||||||
|
|
||||||
|
import { ToastContainerComponent } from './toast-container.component'
|
||||||
|
import { NotificationsToastComponent } from './notifications-toast/notifications-toast.component'
|
||||||
|
import { OfflineToastComponent } from './offline-toast/offline-toast.component'
|
||||||
|
import { RefreshAlertComponent } from './refresh-alert/refresh-alert.component'
|
||||||
|
import { UpdateToastComponent } from './update-toast/update-toast.component'
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [CommonModule, ToastModule, AlertModule, RouterModule],
|
||||||
|
declarations: [
|
||||||
|
ToastContainerComponent,
|
||||||
|
NotificationsToastComponent,
|
||||||
|
OfflineToastComponent,
|
||||||
|
RefreshAlertComponent,
|
||||||
|
UpdateToastComponent,
|
||||||
|
],
|
||||||
|
exports: [ToastContainerComponent],
|
||||||
|
})
|
||||||
|
export class ToastContainerModule {}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<toast
|
||||||
|
*ngIf="visible$ | async as message"
|
||||||
|
class="success-toast"
|
||||||
|
header="EOS download complete!"
|
||||||
|
(dismiss)="onDismiss()"
|
||||||
|
>
|
||||||
|
Restart your Embassy for these updates to take effect. It can take several
|
||||||
|
minutes to come back online.
|
||||||
|
<button toastButton icon="close" side="start" (click)="onDismiss()"></button>
|
||||||
|
<button toastButton side="end" (click)="restart()">Restart</button>
|
||||||
|
</toast>
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
|
||||||
|
import { LoadingController } from '@ionic/angular'
|
||||||
|
import { ErrorToastService } from '@start9labs/shared'
|
||||||
|
import { Observable, Subject, merge } from 'rxjs'
|
||||||
|
|
||||||
|
import { UpdateToastService } from './update-toast.service'
|
||||||
|
import { ApiService } from '../../../services/api/embassy-api.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'update-toast',
|
||||||
|
templateUrl: './update-toast.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class UpdateToastComponent {
|
||||||
|
private readonly dismiss$ = new Subject<boolean>()
|
||||||
|
|
||||||
|
readonly visible$: Observable<boolean> = merge(this.dismiss$, this.update$)
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(UpdateToastService) private readonly update$: Observable<boolean>,
|
||||||
|
private readonly embassyApi: ApiService,
|
||||||
|
private readonly errToast: ErrorToastService,
|
||||||
|
private readonly loadingCtrl: LoadingController,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
onDismiss() {
|
||||||
|
this.dismiss$.next(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
async restart(): Promise<void> {
|
||||||
|
this.onDismiss()
|
||||||
|
|
||||||
|
const loader = await this.loadingCtrl.create({
|
||||||
|
message: 'Restarting...',
|
||||||
|
})
|
||||||
|
|
||||||
|
await loader.present()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.embassyApi.restartServer({})
|
||||||
|
} catch (e: any) {
|
||||||
|
await this.errToast.present(e)
|
||||||
|
} finally {
|
||||||
|
await loader.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
import { endWith, Observable } from 'rxjs'
|
||||||
|
import { distinctUntilChanged, filter } from 'rxjs/operators'
|
||||||
|
import { PatchDbService } from '../../../services/patch-db/patch-db.service'
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class UpdateToastService extends Observable<boolean> {
|
||||||
|
private readonly stream$ = this.patch
|
||||||
|
.watch$('server-info', 'status-info', 'updated')
|
||||||
|
.pipe(distinctUntilChanged(), filter(Boolean), endWith(false))
|
||||||
|
|
||||||
|
constructor(private readonly patch: PatchDbService) {
|
||||||
|
super(subscriber => this.stream$.subscribe(subscriber))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Component } from '@angular/core'
|
import { Component } from '@angular/core'
|
||||||
import { ModalController } from '@ionic/angular'
|
import { ModalController } from '@ionic/angular'
|
||||||
import { map, take } from 'rxjs/operators'
|
import { filter, map, take } from 'rxjs/operators'
|
||||||
import { PackageState } from 'src/app/services/patch-db/data-model'
|
import { PackageState } from 'src/app/services/patch-db/data-model'
|
||||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||||
|
|
||||||
@@ -29,6 +29,7 @@ export class BackupSelectPage {
|
|||||||
this.patch
|
this.patch
|
||||||
.watch$('package-data')
|
.watch$('package-data')
|
||||||
.pipe(
|
.pipe(
|
||||||
|
filter(Boolean),
|
||||||
map(pkgs => {
|
map(pkgs => {
|
||||||
return Object.values(pkgs).map(pkg => {
|
return Object.values(pkgs).map(pkg => {
|
||||||
const { id, title } = pkg.manifest
|
const { id, title } = pkg.manifest
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<ion-header>
|
<ion-header>
|
||||||
<ion-toolbar>
|
<ion-toolbar>
|
||||||
<ion-buttons slot="start">
|
<ion-title>{{ title }}</ion-title>
|
||||||
|
<ion-buttons slot="end">
|
||||||
<ion-button (click)="dismiss()">
|
<ion-button (click)="dismiss()">
|
||||||
<ion-icon slot="icon-only" name="close"></ion-icon>
|
<ion-icon slot="icon-only" name="close"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</ion-buttons>
|
</ion-buttons>
|
||||||
<ion-title>{{ title }}</ion-title>
|
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component, Input } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||||
import { ActivatedRoute } from '@angular/router'
|
import { ActivatedRoute } from '@angular/router'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import {
|
import {
|
||||||
@@ -22,6 +22,7 @@ import { hasCurrentDeps } from 'src/app/util/has-deps'
|
|||||||
selector: 'app-actions',
|
selector: 'app-actions',
|
||||||
templateUrl: './app-actions.page.html',
|
templateUrl: './app-actions.page.html',
|
||||||
styleUrls: ['./app-actions.page.scss'],
|
styleUrls: ['./app-actions.page.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class AppActionsPage {
|
export class AppActionsPage {
|
||||||
readonly pkgId = getPkgId(this.route)
|
readonly pkgId = getPkgId(this.route)
|
||||||
@@ -103,7 +104,7 @@ export class AppActionsPage {
|
|||||||
} else if (last) {
|
} else if (last) {
|
||||||
statusesStr = `${last}`
|
statusesStr = `${last}`
|
||||||
} else {
|
} else {
|
||||||
error = `There is state for which this action may be run. This is a bug. Please file an issue with the service maintainer.`
|
error = `There is no status for which this action may be run. This is a bug. Please file an issue with the service maintainer.`
|
||||||
}
|
}
|
||||||
const alert = await this.alertCtrl.create({
|
const alert = await this.alertCtrl.create({
|
||||||
header: 'Forbidden',
|
header: 'Forbidden',
|
||||||
@@ -158,10 +159,12 @@ export class AppActionsPage {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await this.embassyApi.uninstallPackage({ id: this.pkgId })
|
await this.embassyApi.uninstallPackage({ id: this.pkgId })
|
||||||
this.embassyApi.setDbValue({
|
this.embassyApi
|
||||||
pointer: `/ack-instructions/${this.pkgId}`,
|
.setDbValue({
|
||||||
value: false,
|
pointer: `/ack-instructions/${this.pkgId}`,
|
||||||
})
|
value: false,
|
||||||
|
})
|
||||||
|
.catch(e => console.error('Failed to mark instructions as unseen', e))
|
||||||
this.navCtrl.navigateRoot('/services')
|
this.navCtrl.navigateRoot('/services')
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errToast.present(e)
|
this.errToast.present(e)
|
||||||
@@ -185,7 +188,7 @@ export class AppActionsPage {
|
|||||||
'action-id': actionId,
|
'action-id': actionId,
|
||||||
input,
|
input,
|
||||||
})
|
})
|
||||||
this.modalCtrl.dismiss()
|
|
||||||
const successModal = await this.modalCtrl.create({
|
const successModal = await this.modalCtrl.create({
|
||||||
component: ActionSuccessPage,
|
component: ActionSuccessPage,
|
||||||
componentProps: {
|
componentProps: {
|
||||||
@@ -193,8 +196,8 @@ export class AppActionsPage {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
setTimeout(() => successModal.present(), 400)
|
setTimeout(() => successModal.present(), 500)
|
||||||
return true
|
return false
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errToast.present(e)
|
this.errToast.present(e)
|
||||||
return false
|
return false
|
||||||
@@ -218,6 +221,7 @@ interface LocalAction {
|
|||||||
selector: 'app-actions-item',
|
selector: 'app-actions-item',
|
||||||
templateUrl: './app-actions-item.component.html',
|
templateUrl: './app-actions-item.component.html',
|
||||||
styleUrls: ['./app-actions.page.scss'],
|
styleUrls: ['./app-actions.page.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class AppActionsItemComponent {
|
export class AppActionsItemComponent {
|
||||||
@Input() action!: LocalAction
|
@Input() action!: LocalAction
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
<div
|
<ng-container *ngIf="connected$ | async; else disconnected">
|
||||||
*ngIf="disconnected$ | async; else connected"
|
|
||||||
class="bulb"
|
|
||||||
[style.background-color]="'var(--ion-color-dark)'"
|
|
||||||
></div>
|
|
||||||
<ng-template #connected>
|
|
||||||
<ion-icon
|
<ion-icon
|
||||||
*ngIf="pkg.error; else noError"
|
*ngIf="pkg.error; else noError"
|
||||||
class="warning-icon"
|
class="warning-icon"
|
||||||
@@ -28,4 +23,8 @@
|
|||||||
></div>
|
></div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-template #disconnected>
|
||||||
|
<div class="bulb" [style.background-color]="'var(--ion-color-dark)'"></div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export class AppListIconComponent {
|
|||||||
@Input()
|
@Input()
|
||||||
pkg!: PkgInfo
|
pkg!: PkgInfo
|
||||||
|
|
||||||
disconnected$ = this.connectionService.watchDisconnected$()
|
readonly connected$ = this.connectionService.connected$
|
||||||
|
|
||||||
constructor(private readonly connectionService: ConnectionService) {}
|
constructor(private readonly connectionService: ConnectionService) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { ErrorToastService } from '@start9labs/shared'
|
|||||||
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { from, merge, OperatorFunction, pipe, Subject } from 'rxjs'
|
import { from, merge, OperatorFunction, pipe, Subject } from 'rxjs'
|
||||||
import { catchError, mapTo, startWith, switchMap, tap } from 'rxjs/operators'
|
import { catchError, map, startWith, switchMap, tap } from 'rxjs/operators'
|
||||||
import { RecoveredInfo } from 'src/app/util/parse-data-model'
|
import { RecoveredInfo } from 'src/app/util/parse-data-model'
|
||||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ function loading(
|
|||||||
// Show notification on error
|
// Show notification on error
|
||||||
catchError(e => from(errToast.present(e))),
|
catchError(e => from(errToast.present(e))),
|
||||||
// Map any result to false to stop loading indicator
|
// Map any result to false to stop loading indicator
|
||||||
mapTo(false),
|
map(() => false),
|
||||||
// Start operation with true
|
// Start operation with true
|
||||||
startWith(true),
|
startWith(true),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,12 +9,12 @@
|
|||||||
|
|
||||||
<ion-content class="ion-padding">
|
<ion-content class="ion-padding">
|
||||||
<!-- loading -->
|
<!-- loading -->
|
||||||
<ng-template #loading>
|
<ng-container *ngIf="loading else loaded">
|
||||||
<text-spinner text="Connecting to Embassy"></text-spinner>
|
<text-spinner text="Connecting to Embassy"></text-spinner>
|
||||||
</ng-template>
|
</ng-container>
|
||||||
|
|
||||||
<!-- not loading -->
|
<!-- loaded -->
|
||||||
<ng-container *ngIf="connected$ | async else loading">
|
<ng-template #loaded>
|
||||||
<app-list-empty
|
<app-list-empty
|
||||||
*ngIf="empty; else list"
|
*ngIf="empty; else list"
|
||||||
class="ion-text-center ion-padding"
|
class="ion-text-center ion-padding"
|
||||||
@@ -39,5 +39,5 @@
|
|||||||
</ion-item-group>
|
</ion-item-group>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-container>
|
</ng-template>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|||||||
@@ -14,12 +14,11 @@ import { parseDataModel, RecoveredInfo } from 'src/app/util/parse-data-model'
|
|||||||
providers: [DestroyService],
|
providers: [DestroyService],
|
||||||
})
|
})
|
||||||
export class AppListPage {
|
export class AppListPage {
|
||||||
|
loading = true
|
||||||
pkgs: readonly PackageDataEntry[] = []
|
pkgs: readonly PackageDataEntry[] = []
|
||||||
recoveredPkgs: readonly RecoveredInfo[] = []
|
recoveredPkgs: readonly RecoveredInfo[] = []
|
||||||
reordering = false
|
reordering = false
|
||||||
|
|
||||||
readonly connected$ = this.patch.connected$
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly api: ApiService,
|
private readonly api: ApiService,
|
||||||
private readonly destroy$: DestroyService,
|
private readonly destroy$: DestroyService,
|
||||||
@@ -38,6 +37,7 @@ export class AppListPage {
|
|||||||
take(1),
|
take(1),
|
||||||
map(parseDataModel),
|
map(parseDataModel),
|
||||||
tap(({ pkgs, recoveredPkgs }) => {
|
tap(({ pkgs, recoveredPkgs }) => {
|
||||||
|
this.loading = false
|
||||||
this.pkgs = pkgs
|
this.pkgs = pkgs
|
||||||
this.recoveredPkgs = recoveredPkgs
|
this.recoveredPkgs = recoveredPkgs
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ export class AppShowPage {
|
|||||||
readonly currentMarketplace$: Observable<Marketplace> =
|
readonly currentMarketplace$: Observable<Marketplace> =
|
||||||
this.marketplaceService.getMarketplace()
|
this.marketplaceService.getMarketplace()
|
||||||
|
|
||||||
readonly altMarketplaceData$: Observable<UIMarketplaceData | undefined> =
|
readonly altMarketplaceData$: Observable<UIMarketplaceData> =
|
||||||
this.marketplaceService.getAltMarketplace()
|
this.marketplaceService.getAltMarketplaceData()
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly route: ActivatedRoute,
|
private readonly route: ActivatedRoute,
|
||||||
|
|||||||
@@ -3,19 +3,10 @@
|
|||||||
>
|
>
|
||||||
<ng-container *ngIf="checks.length">
|
<ng-container *ngIf="checks.length">
|
||||||
<ion-item-divider>Health Checks</ion-item-divider>
|
<ion-item-divider>Health Checks</ion-item-divider>
|
||||||
<ng-container *ngIf="disconnected$ | async; else connected">
|
<!-- connected -->
|
||||||
<ion-item *ngFor="let health of checks">
|
<ng-container *ngIf="connected$ | async; else disconnected">
|
||||||
<ion-avatar slot="start">
|
|
||||||
<ion-skeleton-text class="avatar"></ion-skeleton-text>
|
|
||||||
</ion-avatar>
|
|
||||||
<ion-label>
|
|
||||||
<ion-skeleton-text class="label"></ion-skeleton-text>
|
|
||||||
<ion-skeleton-text class="description"></ion-skeleton-text>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
</ng-container>
|
|
||||||
<ng-template #connected>
|
|
||||||
<ion-item *ngFor="let health of checks">
|
<ion-item *ngFor="let health of checks">
|
||||||
|
<!-- result -->
|
||||||
<ng-container *ngIf="health.value?.result as result; else noResult">
|
<ng-container *ngIf="health.value?.result as result; else noResult">
|
||||||
<ion-spinner
|
<ion-spinner
|
||||||
*ngIf="isLoading(result)"
|
*ngIf="isLoading(result)"
|
||||||
@@ -69,6 +60,7 @@
|
|||||||
</ion-text>
|
</ion-text>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
<!-- no result -->
|
||||||
<ng-template #noResult>
|
<ng-template #noResult>
|
||||||
<ion-spinner
|
<ion-spinner
|
||||||
class="icon-spinner"
|
class="icon-spinner"
|
||||||
@@ -83,6 +75,18 @@
|
|||||||
</ion-label>
|
</ion-label>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
</ng-container>
|
||||||
|
<!-- disconnected -->
|
||||||
|
<ng-template #disconnected>
|
||||||
|
<ion-item *ngFor="let health of checks">
|
||||||
|
<ion-avatar slot="start">
|
||||||
|
<ion-skeleton-text class="avatar"></ion-skeleton-text>
|
||||||
|
</ion-avatar>
|
||||||
|
<ion-label>
|
||||||
|
<ion-skeleton-text class="label"></ion-skeleton-text>
|
||||||
|
<ion-skeleton-text class="description"></ion-skeleton-text>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export class AppShowHealthChecksComponent {
|
|||||||
|
|
||||||
HealthResult = HealthResult
|
HealthResult = HealthResult
|
||||||
|
|
||||||
readonly disconnected$ = this.connectionService.watchDisconnected$()
|
readonly connected$ = this.connectionService.connected$
|
||||||
|
|
||||||
constructor(private readonly connectionService: ConnectionService) {}
|
constructor(private readonly connectionService: ConnectionService) {}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<ng-container *ngIf="isInstalled && !(disconnected$ | async)">
|
<ng-container *ngIf="isInstalled && (connected$ | async)">
|
||||||
<ion-grid>
|
<ion-grid>
|
||||||
<ion-row style="padding-left: 12px">
|
<ion-row style="padding-left: 12px">
|
||||||
<ion-col>
|
<ion-col>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export class AppShowStatusComponent {
|
|||||||
|
|
||||||
PR = PrimaryRendering
|
PR = PrimaryRendering
|
||||||
|
|
||||||
disconnected$ = this.connectionService.watchDisconnected$()
|
readonly connected$ = this.connectionService.connected$
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly alertCtrl: AlertController,
|
private readonly alertCtrl: AlertController,
|
||||||
|
|||||||
@@ -117,10 +117,12 @@ export class ToButtonsPipe implements PipeTransform {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async presentModalInstructions(pkg: PackageDataEntry) {
|
private async presentModalInstructions(pkg: PackageDataEntry) {
|
||||||
this.apiService.setDbValue({
|
this.apiService
|
||||||
pointer: `/ack-instructions/${pkg.manifest.id}`,
|
.setDbValue({
|
||||||
value: true,
|
pointer: `/ack-instructions/${pkg.manifest.id}`,
|
||||||
})
|
value: true,
|
||||||
|
})
|
||||||
|
.catch(e => console.error('Failed to mark instructions as seen', e))
|
||||||
|
|
||||||
const modal = await this.modalCtrl.create({
|
const modal = await this.modalCtrl.create({
|
||||||
componentProps: {
|
componentProps: {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
DependencyErrorType,
|
DependencyErrorType,
|
||||||
PackageDataEntry,
|
PackageDataEntry,
|
||||||
} from 'src/app/services/patch-db/data-model'
|
} from 'src/app/services/patch-db/data-model'
|
||||||
import { exists } from '@start9labs/shared'
|
|
||||||
import { DependentInfo } from 'src/app/types/dependent-info'
|
import { DependentInfo } from 'src/app/types/dependent-info'
|
||||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||||
import { ModalService } from 'src/app/services/modal.service'
|
import { ModalService } from 'src/app/services/modal.service'
|
||||||
@@ -49,7 +48,7 @@ export class ToDependenciesPipe implements PipeTransform {
|
|||||||
'dependency-errors',
|
'dependency-errors',
|
||||||
),
|
),
|
||||||
]).pipe(
|
]).pipe(
|
||||||
filter(deps => deps.every(exists) && !!pkg.installed),
|
filter(deps => deps.every(Boolean) && !!pkg.installed),
|
||||||
map(([currentDeps, depErrors]) =>
|
map(([currentDeps, depErrors]) =>
|
||||||
Object.keys(currentDeps)
|
Object.keys(currentDeps)
|
||||||
.filter(id => !!pkg.manifest.dependencies[id])
|
.filter(id => !!pkg.manifest.dependencies[id])
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Component } from '@angular/core'
|
import { Component } from '@angular/core'
|
||||||
import { ActivatedRoute } from '@angular/router'
|
import { ActivatedRoute } from '@angular/router'
|
||||||
import { ModalController } from '@ionic/angular'
|
import { ModalController } from '@ionic/angular'
|
||||||
import { debounce, exists, ErrorToastService } from '@start9labs/shared'
|
import { debounce, ErrorToastService } from '@start9labs/shared'
|
||||||
import * as yaml from 'js-yaml'
|
import * as yaml from 'js-yaml'
|
||||||
import { filter, take } from 'rxjs/operators'
|
import { filter, take } from 'rxjs/operators'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
@@ -31,7 +31,7 @@ export class DevConfigPage {
|
|||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.patchDb
|
this.patchDb
|
||||||
.watch$('ui', 'dev', this.projectId, 'config')
|
.watch$('ui', 'dev', this.projectId, 'config')
|
||||||
.pipe(filter(exists), take(1))
|
.pipe(filter(Boolean), take(1))
|
||||||
.subscribe(config => {
|
.subscribe(config => {
|
||||||
this.code = config
|
this.code = config
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { filter, take } from 'rxjs/operators'
|
|||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import {
|
import {
|
||||||
debounce,
|
debounce,
|
||||||
exists,
|
|
||||||
ErrorToastService,
|
ErrorToastService,
|
||||||
MarkdownComponent,
|
MarkdownComponent,
|
||||||
} from '@start9labs/shared'
|
} from '@start9labs/shared'
|
||||||
@@ -20,8 +19,8 @@ import { getProjectId } from 'src/app/util/get-project-id'
|
|||||||
export class DevInstructionsPage {
|
export class DevInstructionsPage {
|
||||||
readonly projectId = getProjectId(this.route)
|
readonly projectId = getProjectId(this.route)
|
||||||
editorOptions = { theme: 'vs-dark', language: 'markdown' }
|
editorOptions = { theme: 'vs-dark', language: 'markdown' }
|
||||||
code: string = ''
|
code = ''
|
||||||
saving: boolean = false
|
saving = false
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly route: ActivatedRoute,
|
private readonly route: ActivatedRoute,
|
||||||
@@ -34,7 +33,7 @@ export class DevInstructionsPage {
|
|||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.patchDb
|
this.patchDb
|
||||||
.watch$('ui', 'dev', this.projectId, 'instructions')
|
.watch$('ui', 'dev', this.projectId, 'instructions')
|
||||||
.pipe(filter(exists), take(1))
|
.pipe(filter(Boolean), take(1))
|
||||||
.subscribe(config => {
|
.subscribe(config => {
|
||||||
this.code = config
|
this.code = config
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
AlertController,
|
AlertController,
|
||||||
LoadingController,
|
LoadingController,
|
||||||
ModalController,
|
ModalController,
|
||||||
NavController,
|
|
||||||
} from '@ionic/angular'
|
} from '@ionic/angular'
|
||||||
import {
|
import {
|
||||||
GenericInputComponent,
|
GenericInputComponent,
|
||||||
@@ -17,7 +16,6 @@ import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
|||||||
import * as yaml from 'js-yaml'
|
import * as yaml from 'js-yaml'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
import { DevData } from 'src/app/services/patch-db/data-model'
|
import { DevData } from 'src/app/services/patch-db/data-model'
|
||||||
import { ActivatedRoute } from '@angular/router'
|
|
||||||
import { DestroyService, ErrorToastService } from '@start9labs/shared'
|
import { DestroyService, ErrorToastService } from '@start9labs/shared'
|
||||||
import { takeUntil } from 'rxjs/operators'
|
import { takeUntil } from 'rxjs/operators'
|
||||||
|
|
||||||
@@ -36,8 +34,6 @@ export class DeveloperListPage {
|
|||||||
private readonly loadingCtrl: LoadingController,
|
private readonly loadingCtrl: LoadingController,
|
||||||
private readonly errToast: ErrorToastService,
|
private readonly errToast: ErrorToastService,
|
||||||
private readonly alertCtrl: AlertController,
|
private readonly alertCtrl: AlertController,
|
||||||
private readonly navCtrl: NavController,
|
|
||||||
private readonly route: ActivatedRoute,
|
|
||||||
private readonly destroy$: DestroyService,
|
private readonly destroy$: DestroyService,
|
||||||
private readonly patch: PatchDbService,
|
private readonly patch: PatchDbService,
|
||||||
private readonly actionCtrl: ActionSheetController,
|
private readonly actionCtrl: ActionSheetController,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component } from '@angular/core'
|
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||||
import { ActivatedRoute } from '@angular/router'
|
import { ActivatedRoute } from '@angular/router'
|
||||||
import { LoadingController, ModalController } from '@ionic/angular'
|
import { LoadingController, ModalController } from '@ionic/angular'
|
||||||
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
|
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
|
||||||
@@ -13,6 +13,7 @@ import { DevProjectData } from 'src/app/services/patch-db/data-model'
|
|||||||
selector: 'developer-menu',
|
selector: 'developer-menu',
|
||||||
templateUrl: 'developer-menu.page.html',
|
templateUrl: 'developer-menu.page.html',
|
||||||
styleUrls: ['developer-menu.page.scss'],
|
styleUrls: ['developer-menu.page.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class DeveloperMenuPage {
|
export class DeveloperMenuPage {
|
||||||
readonly projectId = getProjectId(this.route)
|
readonly projectId = getProjectId(this.route)
|
||||||
|
|||||||
@@ -3,11 +3,7 @@ import { CommonModule } from '@angular/common'
|
|||||||
import { FormsModule } from '@angular/forms'
|
import { FormsModule } from '@angular/forms'
|
||||||
import { Routes, RouterModule } from '@angular/router'
|
import { Routes, RouterModule } from '@angular/router'
|
||||||
import { IonicModule } from '@ionic/angular'
|
import { IonicModule } from '@ionic/angular'
|
||||||
import {
|
import { SharedPipesModule, EmverPipesModule } from '@start9labs/shared'
|
||||||
SharedPipesModule,
|
|
||||||
EmverPipesModule,
|
|
||||||
TextSpinnerComponentModule,
|
|
||||||
} from '@start9labs/shared'
|
|
||||||
import {
|
import {
|
||||||
FilterPackagesPipeModule,
|
FilterPackagesPipeModule,
|
||||||
CategoriesModule,
|
CategoriesModule,
|
||||||
@@ -16,7 +12,6 @@ import {
|
|||||||
SkeletonModule,
|
SkeletonModule,
|
||||||
} from '@start9labs/marketplace'
|
} from '@start9labs/marketplace'
|
||||||
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||||
|
|
||||||
import { MarketplaceStatusModule } from '../marketplace-status/marketplace-status.module'
|
import { MarketplaceStatusModule } from '../marketplace-status/marketplace-status.module'
|
||||||
import { MarketplaceListPage } from './marketplace-list.page'
|
import { MarketplaceListPage } from './marketplace-list.page'
|
||||||
import { MarketplaceListContentComponent } from './marketplace-list-content/marketplace-list-content.component'
|
import { MarketplaceListContentComponent } from './marketplace-list-content/marketplace-list-content.component'
|
||||||
@@ -34,7 +29,6 @@ const routes: Routes = [
|
|||||||
IonicModule,
|
IonicModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
RouterModule.forChild(routes),
|
RouterModule.forChild(routes),
|
||||||
TextSpinnerComponentModule,
|
|
||||||
SharedPipesModule,
|
SharedPipesModule,
|
||||||
EmverPipesModule,
|
EmverPipesModule,
|
||||||
FilterPackagesPipeModule,
|
FilterPackagesPipeModule,
|
||||||
|
|||||||
@@ -8,14 +8,9 @@
|
|||||||
|
|
||||||
<ion-content class="ion-padding">
|
<ion-content class="ion-padding">
|
||||||
<marketplace-list-content
|
<marketplace-list-content
|
||||||
*ngIf="connected$ | async else loading"
|
|
||||||
[localPkgs]="(localPkgs$ | async) || {}"
|
[localPkgs]="(localPkgs$ | async) || {}"
|
||||||
[pkgs]="pkgs$ | async"
|
[pkgs]="pkgs$ | async"
|
||||||
[categories]="categories$ | async"
|
[categories]="categories$ | async"
|
||||||
[name]="(name$ | async) || ''"
|
[name]="(name$ | async) || ''"
|
||||||
></marketplace-list-content>
|
></marketplace-list-content>
|
||||||
|
|
||||||
<ng-template #loading>
|
|
||||||
<text-spinner text="Connecting to Embassy"></text-spinner>
|
|
||||||
</ng-template>
|
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|||||||
@@ -1,45 +1,30 @@
|
|||||||
import { Component } from '@angular/core'
|
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||||
import { Observable } from 'rxjs'
|
import { map } from 'rxjs/operators'
|
||||||
import { filter, first, map, startWith, switchMapTo } from 'rxjs/operators'
|
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
||||||
import { exists, isEmptyObject } from '@start9labs/shared'
|
|
||||||
import {
|
|
||||||
AbstractMarketplaceService,
|
|
||||||
MarketplacePkg,
|
|
||||||
} from '@start9labs/marketplace'
|
|
||||||
|
|
||||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
import { ConnectionService } from 'src/app/services/connection.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'marketplace-list',
|
selector: 'marketplace-list',
|
||||||
templateUrl: './marketplace-list.page.html',
|
templateUrl: './marketplace-list.page.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class MarketplaceListPage {
|
export class MarketplaceListPage {
|
||||||
readonly connected$ = this.patch.connected$
|
readonly connected$ = this.connectionService.connected$
|
||||||
|
|
||||||
readonly localPkgs$: Observable<Record<string, PackageDataEntry>> = this.patch
|
readonly localPkgs$ = this.patch.watch$('package-data')
|
||||||
.watch$('package-data')
|
|
||||||
.pipe(
|
|
||||||
filter(data => exists(data) && !isEmptyObject(data)),
|
|
||||||
startWith({}),
|
|
||||||
)
|
|
||||||
|
|
||||||
readonly categories$ = this.marketplaceService.getCategories()
|
readonly categories$ = this.marketplaceService.getCategories()
|
||||||
|
|
||||||
readonly pkgs$: Observable<MarketplacePkg[]> = this.patch
|
readonly pkgs$ = this.marketplaceService.getPackages()
|
||||||
.watch$('server-info')
|
|
||||||
.pipe(
|
|
||||||
filter(data => exists(data) && !isEmptyObject(data)),
|
|
||||||
first(),
|
|
||||||
switchMapTo(this.marketplaceService.getPackages()),
|
|
||||||
)
|
|
||||||
|
|
||||||
readonly name$: Observable<string> = this.marketplaceService
|
readonly name$ = this.marketplaceService
|
||||||
.getMarketplace()
|
.getMarketplace()
|
||||||
.pipe(map(({ name }) => name))
|
.pipe(map(({ name }) => name))
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly patch: PatchDbService,
|
private readonly patch: PatchDbService,
|
||||||
private readonly marketplaceService: AbstractMarketplaceService,
|
private readonly marketplaceService: AbstractMarketplaceService,
|
||||||
|
private readonly connectionService: ConnectionService,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component } from '@angular/core'
|
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import {
|
import {
|
||||||
ServerNotifications,
|
ServerNotifications,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component } from '@angular/core'
|
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
import { ConfigService } from 'src/app/services/config.service'
|
||||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||||
|
|
||||||
@@ -6,6 +6,7 @@ import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
|||||||
selector: 'lan',
|
selector: 'lan',
|
||||||
templateUrl: './lan.page.html',
|
templateUrl: './lan.page.html',
|
||||||
styleUrls: ['./lan.page.scss'],
|
styleUrls: ['./lan.page.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class LANPage {
|
export class LANPage {
|
||||||
readonly downloadIsDisabled = !this.config.isTor()
|
readonly downloadIsDisabled = !this.config.isTor()
|
||||||
|
|||||||
@@ -11,12 +11,9 @@
|
|||||||
<ng-container *ngIf="ui$ | async as ui">
|
<ng-container *ngIf="ui$ | async as ui">
|
||||||
<ion-item-group *ngIf="server$ | async as server">
|
<ion-item-group *ngIf="server$ | async as server">
|
||||||
<ion-item-divider>General</ion-item-divider>
|
<ion-item-divider>General</ion-item-divider>
|
||||||
<ion-item
|
<ion-item button (click)="presentModalName('My Embassy', ui.name)">
|
||||||
button
|
|
||||||
(click)="presentModalName('Embassy-' + server.id, ui.name)"
|
|
||||||
>
|
|
||||||
<ion-label>Device Name</ion-label>
|
<ion-label>Device Name</ion-label>
|
||||||
<ion-note slot="end">{{ ui.name || 'Embassy-' + server.id }}</ion-note>
|
<ion-note slot="end">{{ ui.name || 'My Embassy' }}</ion-note>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<ion-item-divider>Marketplace</ion-item-divider>
|
<ion-item-divider>Marketplace</ion-item-divider>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component } from '@angular/core'
|
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||||
import {
|
import {
|
||||||
LoadingController,
|
LoadingController,
|
||||||
@@ -17,6 +17,7 @@ import { LocalStorageService } from '../../../services/local-storage.service'
|
|||||||
selector: 'preferences',
|
selector: 'preferences',
|
||||||
templateUrl: './preferences.page.html',
|
templateUrl: './preferences.page.html',
|
||||||
styleUrls: ['./preferences.page.scss'],
|
styleUrls: ['./preferences.page.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class PreferencesPage {
|
export class PreferencesPage {
|
||||||
clicks = 0
|
clicks = 0
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
PipeTransform,
|
PipeTransform,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||||
import { take } from 'rxjs/operators'
|
import { filter, take } from 'rxjs/operators'
|
||||||
import { PackageMainStatus } from 'src/app/services/patch-db/data-model'
|
import { PackageMainStatus } from 'src/app/services/patch-db/data-model'
|
||||||
import { Observable } from 'rxjs'
|
import { Observable } from 'rxjs'
|
||||||
|
|
||||||
@@ -15,7 +15,9 @@ import { Observable } from 'rxjs'
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class BackingUpComponent {
|
export class BackingUpComponent {
|
||||||
readonly pkgs$ = this.patch.watch$('package-data').pipe(take(1))
|
readonly pkgs$ = this.patch
|
||||||
|
.watch$('package-data')
|
||||||
|
.pipe(filter(Boolean), take(1))
|
||||||
readonly backupProgress$ = this.patch.watch$(
|
readonly backupProgress$ = this.patch.watch$(
|
||||||
'server-info',
|
'server-info',
|
||||||
'status-info',
|
'status-info',
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
<ion-header>
|
<ion-header>
|
||||||
<ion-toolbar>
|
<ion-toolbar>
|
||||||
<ion-title *ngIf="connected$ | async else loading">
|
<ion-title *ngIf="ui$ | async as ui; else loadingTitle">
|
||||||
{{ (ui$ | async)?.name || "Embassy-" + (server$ | async)?.id }}
|
{{ ui.name || "My Embassy" }}
|
||||||
</ion-title>
|
</ion-title>
|
||||||
<ng-template #loading>
|
<ng-template #loadingTitle>
|
||||||
<ion-title>Loading<span class="loading-dots"></span></ion-title>
|
<ion-title>
|
||||||
|
<ion-title>Loading<span class="loading-dots"></span></ion-title>
|
||||||
|
</ion-title>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ion-buttons slot="end">
|
<ion-buttons slot="end">
|
||||||
<badge-menu-button></badge-menu-button>
|
<badge-menu-button></badge-menu-button>
|
||||||
@@ -14,86 +16,80 @@
|
|||||||
|
|
||||||
<ion-content class="ion-padding">
|
<ion-content class="ion-padding">
|
||||||
<!-- loading -->
|
<!-- loading -->
|
||||||
<ng-template #spinner>
|
<ng-template #loading>
|
||||||
<text-spinner text="Connecting to Embassy"></text-spinner>
|
<text-spinner text="Connecting to Embassy"></text-spinner>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<!-- not loading -->
|
<!-- loaded -->
|
||||||
<ng-container *ngIf="connected$ | async else spinner">
|
<ion-item-group *ngIf="server$ | async as server; else loading">
|
||||||
<ion-item-group *ngIf="server$ | async as server">
|
<div *ngFor="let cat of settings | keyvalue : asIsOrder">
|
||||||
<div *ngFor="let cat of settings | keyvalue : asIsOrder">
|
<ion-item-divider>
|
||||||
<ion-item-divider>
|
<ion-text color="dark" *ngIf="cat.key !== 'Power'">
|
||||||
<ion-text color="dark" *ngIf="cat.key !== 'Power'">
|
{{ cat.key }}
|
||||||
{{ cat.key }}
|
</ion-text>
|
||||||
</ion-text>
|
<ion-text color="dark" *ngIf="cat.key === 'Power'" (click)="addClick()">
|
||||||
<ion-text
|
{{ cat.key }}
|
||||||
color="dark"
|
</ion-text>
|
||||||
*ngIf="cat.key === 'Power'"
|
</ion-item-divider>
|
||||||
(click)="addClick()"
|
<ng-container *ngFor="let button of cat.value">
|
||||||
>
|
<ion-item
|
||||||
{{ cat.key }}
|
button
|
||||||
</ion-text>
|
[style.display]="(button.title === 'Repair Disk' && !(showDiskRepair$ | async)) ? 'none' : 'block'"
|
||||||
</ion-item-divider>
|
[detail]="button.detail"
|
||||||
<ng-container *ngFor="let button of cat.value">
|
[disabled]="button.disabled$ | async"
|
||||||
<ion-item
|
(click)="button.action()"
|
||||||
button
|
>
|
||||||
[style.display]="(button.title === 'Repair Disk' && !(showDiskRepair$ | async)) ? 'none' : 'block'"
|
<ion-icon slot="start" [name]="button.icon"></ion-icon>
|
||||||
[detail]="button.detail"
|
<ion-label>
|
||||||
[disabled]="button.disabled | async"
|
<h2>{{ button.title }}</h2>
|
||||||
(click)="button.action()"
|
<p *ngIf="button.description">{{ button.description }}</p>
|
||||||
>
|
|
||||||
<ion-icon slot="start" [name]="button.icon"></ion-icon>
|
|
||||||
<ion-label>
|
|
||||||
<h2>{{ button.title }}</h2>
|
|
||||||
<p *ngIf="button.description">{{ button.description }}</p>
|
|
||||||
|
|
||||||
<!-- "Create Backup" button only -->
|
<!-- "Create Backup" button only -->
|
||||||
<p *ngIf="button.title === 'Create Backup'">
|
<p *ngIf="button.title === 'Create Backup'">
|
||||||
<ng-container *ngIf="server['status-info'] as statusInfo">
|
<ng-container *ngIf="server['status-info'] as statusInfo">
|
||||||
<ion-text
|
|
||||||
color="warning"
|
|
||||||
*ngIf="!statusInfo['backup-progress'] && !statusInfo['update-progress']"
|
|
||||||
>
|
|
||||||
Last Backup: {{ server['last-backup'] ?
|
|
||||||
(server['last-backup'] | date: 'medium') : 'never' }}
|
|
||||||
</ion-text>
|
|
||||||
<span *ngIf="!!statusInfo['backup-progress']" class="inline">
|
|
||||||
<ion-spinner
|
|
||||||
color="success"
|
|
||||||
style="height: 12px; width: 12px; margin-right: 6px"
|
|
||||||
></ion-spinner>
|
|
||||||
<ion-text color="success">Backing up</ion-text>
|
|
||||||
</span>
|
|
||||||
</ng-container>
|
|
||||||
</p>
|
|
||||||
<!-- "Software Update" button only -->
|
|
||||||
<p *ngIf="button.title === 'Software Update'">
|
|
||||||
<ion-text
|
<ion-text
|
||||||
*ngIf="server['status-info'].updated; else notUpdated"
|
|
||||||
class="inline"
|
|
||||||
color="warning"
|
color="warning"
|
||||||
|
*ngIf="!statusInfo['backup-progress'] && !statusInfo['update-progress']"
|
||||||
>
|
>
|
||||||
Update Complete. Restart to apply changes
|
Last Backup: {{ server['last-backup'] ? (server['last-backup']
|
||||||
|
| date: 'medium') : 'never' }}
|
||||||
</ion-text>
|
</ion-text>
|
||||||
<ng-template #notUpdated>
|
<span *ngIf="!!statusInfo['backup-progress']" class="inline">
|
||||||
<ng-container *ngIf="showUpdate$ | async; else check">
|
<ion-spinner
|
||||||
<ion-text class="inline" color="success">
|
color="success"
|
||||||
<ion-icon name="rocket-outline"></ion-icon>
|
style="height: 12px; width: 12px; margin-right: 6px"
|
||||||
Update Available
|
></ion-spinner>
|
||||||
</ion-text>
|
<ion-text color="success">Backing up</ion-text>
|
||||||
</ng-container>
|
</span>
|
||||||
<ng-template #check>
|
</ng-container>
|
||||||
<ion-text class="inline" color="dark">
|
</p>
|
||||||
<ion-icon name="refresh"></ion-icon>
|
<!-- "Software Update" button only -->
|
||||||
Check for updates
|
<p *ngIf="button.title === 'Software Update'">
|
||||||
</ion-text>
|
<ion-text
|
||||||
</ng-template>
|
*ngIf="server['status-info'].updated; else notUpdated"
|
||||||
|
class="inline"
|
||||||
|
color="warning"
|
||||||
|
>
|
||||||
|
Update Complete. Restart to apply changes
|
||||||
|
</ion-text>
|
||||||
|
<ng-template #notUpdated>
|
||||||
|
<ng-container *ngIf="showUpdate$ | async; else check">
|
||||||
|
<ion-text class="inline" color="success">
|
||||||
|
<ion-icon name="rocket-outline"></ion-icon>
|
||||||
|
Update Available
|
||||||
|
</ion-text>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #check>
|
||||||
|
<ion-text class="inline" color="dark">
|
||||||
|
<ion-icon name="refresh"></ion-icon>
|
||||||
|
Check for updates
|
||||||
|
</ion-text>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</p>
|
</ng-template>
|
||||||
</ion-label>
|
</p>
|
||||||
</ion-item>
|
</ion-label>
|
||||||
</ng-container>
|
</ion-item>
|
||||||
</div>
|
</ng-container>
|
||||||
</ion-item-group>
|
</div>
|
||||||
</ng-container>
|
</ion-item-group>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|||||||
@@ -9,11 +9,10 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
|
|||||||
import { ActivatedRoute } from '@angular/router'
|
import { ActivatedRoute } from '@angular/router'
|
||||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||||
import { Observable, of } from 'rxjs'
|
import { Observable, of } from 'rxjs'
|
||||||
import { filter, take } from 'rxjs/operators'
|
import { filter, take, tap } from 'rxjs/operators'
|
||||||
import { exists, isEmptyObject, ErrorToastService } from '@start9labs/shared'
|
import { isEmptyObject, ErrorToastService } from '@start9labs/shared'
|
||||||
import { EOSService } from 'src/app/services/eos.service'
|
import { EOSService } from 'src/app/services/eos.service'
|
||||||
import { LocalStorageService } from 'src/app/services/local-storage.service'
|
import { LocalStorageService } from 'src/app/services/local-storage.service'
|
||||||
import { RecoveredPackageDataEntry } from 'src/app/services/patch-db/data-model'
|
|
||||||
import { OSUpdatePage } from 'src/app/modals/os-update/os-update.page'
|
import { OSUpdatePage } from 'src/app/modals/os-update/os-update.page'
|
||||||
import { getAllPackages } from '../../../util/get-package-data'
|
import { getAllPackages } from '../../../util/get-package-data'
|
||||||
|
|
||||||
@@ -28,7 +27,6 @@ export class ServerShowPage {
|
|||||||
|
|
||||||
readonly server$ = this.patch.watch$('server-info')
|
readonly server$ = this.patch.watch$('server-info')
|
||||||
readonly ui$ = this.patch.watch$('ui')
|
readonly ui$ = this.patch.watch$('ui')
|
||||||
readonly connected$ = this.patch.connected$
|
|
||||||
readonly showUpdate$ = this.eosService.showUpdate$
|
readonly showUpdate$ = this.eosService.showUpdate$
|
||||||
readonly showDiskRepair$ = this.localStorageService.showDiskRepair$
|
readonly showDiskRepair$ = this.localStorageService.showDiskRepair$
|
||||||
|
|
||||||
@@ -48,10 +46,12 @@ export class ServerShowPage {
|
|||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.patch
|
this.patch
|
||||||
.watch$('recovered-packages')
|
.watch$('recovered-packages')
|
||||||
.pipe(filter(exists), take(1))
|
.pipe(
|
||||||
.subscribe((rps: { [id: string]: RecoveredPackageDataEntry }) => {
|
filter(Boolean),
|
||||||
this.hasRecoveredPackage = !isEmptyObject(rps)
|
take(1),
|
||||||
})
|
tap(data => (this.hasRecoveredPackage = !isEmptyObject(data))),
|
||||||
|
)
|
||||||
|
.subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateEos(): Promise<void> {
|
async updateEos(): Promise<void> {
|
||||||
@@ -290,7 +290,7 @@ export class ServerShowPage {
|
|||||||
action: () =>
|
action: () =>
|
||||||
this.navCtrl.navigateForward(['backup'], { relativeTo: this.route }),
|
this.navCtrl.navigateForward(['backup'], { relativeTo: this.route }),
|
||||||
detail: true,
|
detail: true,
|
||||||
disabled: of(false),
|
disabled$: of(false),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Restore From Backup',
|
title: 'Restore From Backup',
|
||||||
@@ -299,7 +299,7 @@ export class ServerShowPage {
|
|||||||
action: () =>
|
action: () =>
|
||||||
this.navCtrl.navigateForward(['restore'], { relativeTo: this.route }),
|
this.navCtrl.navigateForward(['restore'], { relativeTo: this.route }),
|
||||||
detail: true,
|
detail: true,
|
||||||
disabled: this.eosService.updatingOrBackingUp$,
|
disabled$: this.eosService.updatingOrBackingUp$,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
Settings: [
|
Settings: [
|
||||||
@@ -312,7 +312,7 @@ export class ServerShowPage {
|
|||||||
? this.updateEos()
|
? this.updateEos()
|
||||||
: this.checkForEosUpdate(),
|
: this.checkForEosUpdate(),
|
||||||
detail: false,
|
detail: false,
|
||||||
disabled: this.eosService.updatingOrBackingUp$,
|
disabled$: this.eosService.updatingOrBackingUp$,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Preferences',
|
title: 'Preferences',
|
||||||
@@ -323,7 +323,7 @@ export class ServerShowPage {
|
|||||||
relativeTo: this.route,
|
relativeTo: this.route,
|
||||||
}),
|
}),
|
||||||
detail: true,
|
detail: true,
|
||||||
disabled: of(false),
|
disabled$: of(false),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'LAN',
|
title: 'LAN',
|
||||||
@@ -332,7 +332,7 @@ export class ServerShowPage {
|
|||||||
action: () =>
|
action: () =>
|
||||||
this.navCtrl.navigateForward(['lan'], { relativeTo: this.route }),
|
this.navCtrl.navigateForward(['lan'], { relativeTo: this.route }),
|
||||||
detail: true,
|
detail: true,
|
||||||
disabled: of(false),
|
disabled$: of(false),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'SSH',
|
title: 'SSH',
|
||||||
@@ -341,7 +341,7 @@ export class ServerShowPage {
|
|||||||
action: () =>
|
action: () =>
|
||||||
this.navCtrl.navigateForward(['ssh'], { relativeTo: this.route }),
|
this.navCtrl.navigateForward(['ssh'], { relativeTo: this.route }),
|
||||||
detail: true,
|
detail: true,
|
||||||
disabled: of(false),
|
disabled$: of(false),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'WiFi',
|
title: 'WiFi',
|
||||||
@@ -350,7 +350,7 @@ export class ServerShowPage {
|
|||||||
action: () =>
|
action: () =>
|
||||||
this.navCtrl.navigateForward(['wifi'], { relativeTo: this.route }),
|
this.navCtrl.navigateForward(['wifi'], { relativeTo: this.route }),
|
||||||
detail: true,
|
detail: true,
|
||||||
disabled: of(false),
|
disabled$: of(false),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Sideload Service',
|
title: 'Sideload Service',
|
||||||
@@ -361,7 +361,7 @@ export class ServerShowPage {
|
|||||||
relativeTo: this.route,
|
relativeTo: this.route,
|
||||||
}),
|
}),
|
||||||
detail: true,
|
detail: true,
|
||||||
disabled: of(false),
|
disabled$: of(false),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Marketplace Settings',
|
title: 'Marketplace Settings',
|
||||||
@@ -372,7 +372,7 @@ export class ServerShowPage {
|
|||||||
relativeTo: this.route,
|
relativeTo: this.route,
|
||||||
}),
|
}),
|
||||||
detail: true,
|
detail: true,
|
||||||
disabled: of(false),
|
disabled$: of(false),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
Insights: [
|
Insights: [
|
||||||
@@ -383,7 +383,7 @@ export class ServerShowPage {
|
|||||||
action: () =>
|
action: () =>
|
||||||
this.navCtrl.navigateForward(['specs'], { relativeTo: this.route }),
|
this.navCtrl.navigateForward(['specs'], { relativeTo: this.route }),
|
||||||
detail: true,
|
detail: true,
|
||||||
disabled: of(false),
|
disabled$: of(false),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Monitor',
|
title: 'Monitor',
|
||||||
@@ -392,7 +392,7 @@ export class ServerShowPage {
|
|||||||
action: () =>
|
action: () =>
|
||||||
this.navCtrl.navigateForward(['metrics'], { relativeTo: this.route }),
|
this.navCtrl.navigateForward(['metrics'], { relativeTo: this.route }),
|
||||||
detail: true,
|
detail: true,
|
||||||
disabled: of(false),
|
disabled$: of(false),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Active Sessions',
|
title: 'Active Sessions',
|
||||||
@@ -403,7 +403,7 @@ export class ServerShowPage {
|
|||||||
relativeTo: this.route,
|
relativeTo: this.route,
|
||||||
}),
|
}),
|
||||||
detail: true,
|
detail: true,
|
||||||
disabled: of(false),
|
disabled$: of(false),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'OS Logs',
|
title: 'OS Logs',
|
||||||
@@ -412,7 +412,7 @@ export class ServerShowPage {
|
|||||||
action: () =>
|
action: () =>
|
||||||
this.navCtrl.navigateForward(['logs'], { relativeTo: this.route }),
|
this.navCtrl.navigateForward(['logs'], { relativeTo: this.route }),
|
||||||
detail: true,
|
detail: true,
|
||||||
disabled: of(false),
|
disabled$: of(false),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Kernel Logs',
|
title: 'Kernel Logs',
|
||||||
@@ -424,7 +424,7 @@ export class ServerShowPage {
|
|||||||
relativeTo: this.route,
|
relativeTo: this.route,
|
||||||
}),
|
}),
|
||||||
detail: true,
|
detail: true,
|
||||||
disabled: of(false),
|
disabled$: of(false),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
Support: [
|
Support: [
|
||||||
@@ -439,7 +439,7 @@ export class ServerShowPage {
|
|||||||
'noreferrer',
|
'noreferrer',
|
||||||
),
|
),
|
||||||
detail: true,
|
detail: true,
|
||||||
disabled: of(false),
|
disabled$: of(false),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Contact Support',
|
title: 'Contact Support',
|
||||||
@@ -452,7 +452,7 @@ export class ServerShowPage {
|
|||||||
'noreferrer',
|
'noreferrer',
|
||||||
),
|
),
|
||||||
detail: true,
|
detail: true,
|
||||||
disabled: of(false),
|
disabled$: of(false),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
Power: [
|
Power: [
|
||||||
@@ -462,7 +462,7 @@ export class ServerShowPage {
|
|||||||
icon: 'reload',
|
icon: 'reload',
|
||||||
action: () => this.presentAlertRestart(),
|
action: () => this.presentAlertRestart(),
|
||||||
detail: false,
|
detail: false,
|
||||||
disabled: of(false),
|
disabled$: of(false),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Shutdown',
|
title: 'Shutdown',
|
||||||
@@ -470,7 +470,7 @@ export class ServerShowPage {
|
|||||||
icon: 'power',
|
icon: 'power',
|
||||||
action: () => this.presentAlertShutdown(),
|
action: () => this.presentAlertShutdown(),
|
||||||
detail: false,
|
detail: false,
|
||||||
disabled: of(false),
|
disabled$: of(false),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'System Rebuild',
|
title: 'System Rebuild',
|
||||||
@@ -478,7 +478,7 @@ export class ServerShowPage {
|
|||||||
icon: 'construct-outline',
|
icon: 'construct-outline',
|
||||||
action: () => this.presentAlertSystemRebuild(),
|
action: () => this.presentAlertSystemRebuild(),
|
||||||
detail: false,
|
detail: false,
|
||||||
disabled: of(false),
|
disabled$: of(false),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Repair Disk',
|
title: 'Repair Disk',
|
||||||
@@ -486,7 +486,7 @@ export class ServerShowPage {
|
|||||||
icon: 'medkit-outline',
|
icon: 'medkit-outline',
|
||||||
action: () => this.presentAlertRepairDisk(),
|
action: () => this.presentAlertRepairDisk(),
|
||||||
detail: false,
|
detail: false,
|
||||||
disabled: of(false),
|
disabled$: of(false),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -517,5 +517,5 @@ interface SettingBtn {
|
|||||||
icon: string
|
icon: string
|
||||||
action: Function
|
action: Function
|
||||||
detail: boolean
|
detail: boolean
|
||||||
disabled: Observable<boolean>
|
disabled$: Observable<boolean>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component } from '@angular/core'
|
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||||
import { ToastController } from '@ionic/angular'
|
import { ToastController } from '@ionic/angular'
|
||||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
import { ConfigService } from 'src/app/services/config.service'
|
||||||
@@ -8,6 +8,7 @@ import { copyToClipboard } from '@start9labs/shared'
|
|||||||
selector: 'server-specs',
|
selector: 'server-specs',
|
||||||
templateUrl: './server-specs.page.html',
|
templateUrl: './server-specs.page.html',
|
||||||
styleUrls: ['./server-specs.page.scss'],
|
styleUrls: ['./server-specs.page.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class ServerSpecsPage {
|
export class ServerSpecsPage {
|
||||||
readonly server$ = this.patch.watch$('server-info')
|
readonly server$ = this.patch.watch$('server-info')
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ export module RR {
|
|||||||
|
|
||||||
// server
|
// server
|
||||||
|
|
||||||
|
export type EchoReq = { message: string } // server.echo
|
||||||
|
export type EchoRes = string
|
||||||
|
|
||||||
export type GetServerLogsReq = ServerLogsReq // server.logs & server.kernel-logs
|
export type GetServerLogsReq = ServerLogsReq // server.logs & server.kernel-logs
|
||||||
export type GetServerLogsRes = LogsRes
|
export type GetServerLogsRes = LogsRes
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +1,12 @@
|
|||||||
import { Subject, Observable } from 'rxjs'
|
import { Subject, Observable } from 'rxjs'
|
||||||
import {
|
import { Update, Operation, Revision } from 'patch-db-client'
|
||||||
Http,
|
|
||||||
Update,
|
|
||||||
Operation,
|
|
||||||
Revision,
|
|
||||||
Source,
|
|
||||||
Store,
|
|
||||||
RPCResponse,
|
|
||||||
} from 'patch-db-client'
|
|
||||||
import { RR } from './api.types'
|
import { RR } from './api.types'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
import { Log, RequestError } from '@start9labs/shared'
|
import { Log, RequestError } from '@start9labs/shared'
|
||||||
import { map } from 'rxjs/operators'
|
|
||||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||||
|
|
||||||
export abstract class ApiService implements Source<DataModel>, Http<DataModel> {
|
export abstract class ApiService {
|
||||||
protected readonly sync$ = new Subject<Update<DataModel>>()
|
readonly sync$ = new Subject<Update<DataModel>>()
|
||||||
|
|
||||||
/** PatchDb Source interface. Post/Patch requests provide a source of patches to the db. */
|
|
||||||
// sequenceStream '_' is not used by the live api, but is overridden by the mock
|
|
||||||
watch$(_?: Store<DataModel>): Observable<RPCResponse<Update<DataModel>>> {
|
|
||||||
return this.sync$
|
|
||||||
.asObservable()
|
|
||||||
.pipe(map(result => ({ result, jsonrpc: '2.0' })))
|
|
||||||
}
|
|
||||||
|
|
||||||
// websocket
|
|
||||||
|
|
||||||
abstract openLogsWebsocket$(
|
|
||||||
config: WebSocketSubjectConfig<Log>,
|
|
||||||
): Observable<Log>
|
|
||||||
|
|
||||||
// http
|
// http
|
||||||
|
|
||||||
@@ -63,6 +40,14 @@ export abstract class ApiService implements Source<DataModel>, Http<DataModel> {
|
|||||||
|
|
||||||
// server
|
// server
|
||||||
|
|
||||||
|
abstract echo(params: RR.EchoReq): Promise<RR.EchoRes>
|
||||||
|
|
||||||
|
abstract openPatchWebsocket$(): Observable<Update<DataModel>>
|
||||||
|
|
||||||
|
abstract openLogsWebsocket$(
|
||||||
|
config: WebSocketSubjectConfig<Log>,
|
||||||
|
): Observable<Log>
|
||||||
|
|
||||||
abstract getServerLogs(
|
abstract getServerLogs(
|
||||||
params: RR.GetServerLogsReq,
|
params: RR.GetServerLogsReq,
|
||||||
): Promise<RR.GetServerLogsRes>
|
): Promise<RR.GetServerLogsRes>
|
||||||
|
|||||||
@@ -1,26 +1,34 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Inject, Injectable } from '@angular/core'
|
||||||
import { HttpService, Log, LogsRes, Method } from '@start9labs/shared'
|
import {
|
||||||
|
HttpService,
|
||||||
|
Log,
|
||||||
|
Method,
|
||||||
|
RPCError,
|
||||||
|
RPCOptions,
|
||||||
|
} from '@start9labs/shared'
|
||||||
import { ApiService } from './embassy-api.service'
|
import { ApiService } from './embassy-api.service'
|
||||||
import { RR } from './api.types'
|
import { RR } from './api.types'
|
||||||
import { parsePropertiesPermissive } from 'src/app/util/properties.util'
|
import { parsePropertiesPermissive } from 'src/app/util/properties.util'
|
||||||
import { ConfigService } from '../config.service'
|
import { ConfigService } from '../config.service'
|
||||||
import { webSocket, WebSocketSubjectConfig } from 'rxjs/webSocket'
|
import { webSocket, WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||||
import { Observable } from 'rxjs'
|
import { Observable, timeout } from 'rxjs'
|
||||||
|
import { AuthService } from '../auth.service'
|
||||||
|
import { DOCUMENT } from '@angular/common'
|
||||||
|
import { DataModel } from '../patch-db/data-model'
|
||||||
|
import { Update } from 'patch-db-client'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LiveApiService extends ApiService {
|
export class LiveApiService extends ApiService {
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DOCUMENT) private readonly document: Document,
|
||||||
private readonly http: HttpService,
|
private readonly http: HttpService,
|
||||||
private readonly config: ConfigService,
|
private readonly config: ConfigService,
|
||||||
|
private readonly auth: AuthService,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
; (window as any).rpcClient = this
|
; (window as any).rpcClient = this
|
||||||
}
|
}
|
||||||
|
|
||||||
openLogsWebsocket$(config: WebSocketSubjectConfig<Log>): Observable<Log> {
|
|
||||||
return webSocket(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
async getStatic(url: string): Promise<string> {
|
async getStatic(url: string): Promise<string> {
|
||||||
return this.http.httpRequest({
|
return this.http.httpRequest({
|
||||||
method: Method.GET,
|
method: Method.GET,
|
||||||
@@ -41,93 +49,114 @@ export class LiveApiService extends ApiService {
|
|||||||
// db
|
// db
|
||||||
|
|
||||||
async getRevisions(since: number): Promise<RR.GetRevisionsRes> {
|
async getRevisions(since: number): Promise<RR.GetRevisionsRes> {
|
||||||
return this.http.rpcRequest({ method: 'db.revisions', params: { since } })
|
return this.rpcRequest({ method: 'db.revisions', params: { since } })
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDump(): Promise<RR.GetDumpRes> {
|
async getDump(): Promise<RR.GetDumpRes> {
|
||||||
return this.http.rpcRequest({ method: 'db.dump', params: {} })
|
return this.rpcRequest({ method: 'db.dump', params: {} })
|
||||||
}
|
}
|
||||||
|
|
||||||
async setDbValueRaw(params: RR.SetDBValueReq): Promise<RR.SetDBValueRes> {
|
async setDbValueRaw(params: RR.SetDBValueReq): Promise<RR.SetDBValueRes> {
|
||||||
return this.http.rpcRequest({ method: 'db.put.ui', params })
|
return this.rpcRequest({ method: 'db.put.ui', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
// auth
|
// auth
|
||||||
|
|
||||||
async login(params: RR.LoginReq): Promise<RR.loginRes> {
|
async login(params: RR.LoginReq): Promise<RR.loginRes> {
|
||||||
return this.http.rpcRequest({ method: 'auth.login', params })
|
return this.rpcRequest({ method: 'auth.login', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async logout(params: RR.LogoutReq): Promise<RR.LogoutRes> {
|
async logout(params: RR.LogoutReq): Promise<RR.LogoutRes> {
|
||||||
return this.http.rpcRequest({ method: 'auth.logout', params })
|
return this.rpcRequest({ method: 'auth.logout', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSessions(params: RR.GetSessionsReq): Promise<RR.GetSessionsRes> {
|
async getSessions(params: RR.GetSessionsReq): Promise<RR.GetSessionsRes> {
|
||||||
return this.http.rpcRequest({ method: 'auth.session.list', params })
|
return this.rpcRequest({ method: 'auth.session.list', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async killSessions(params: RR.KillSessionsReq): Promise<RR.KillSessionsRes> {
|
async killSessions(params: RR.KillSessionsReq): Promise<RR.KillSessionsRes> {
|
||||||
return this.http.rpcRequest({ method: 'auth.session.kill', params })
|
return this.rpcRequest({ method: 'auth.session.kill', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
// server
|
// server
|
||||||
|
|
||||||
|
async echo(params: RR.EchoReq): Promise<RR.EchoRes> {
|
||||||
|
return this.rpcRequest({ method: 'echo', params })
|
||||||
|
}
|
||||||
|
|
||||||
|
openPatchWebsocket$(): Observable<Update<DataModel>> {
|
||||||
|
const config: WebSocketSubjectConfig<Update<DataModel>> = {
|
||||||
|
url: `/db`,
|
||||||
|
closeObserver: {
|
||||||
|
next: val => {
|
||||||
|
if (val.reason === 'UNAUTHORIZED') this.auth.setUnverified()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.openWebsocket(config).pipe(timeout({ first: 21000 }))
|
||||||
|
}
|
||||||
|
|
||||||
|
openLogsWebsocket$(config: WebSocketSubjectConfig<Log>): Observable<Log> {
|
||||||
|
return this.openWebsocket(config)
|
||||||
|
}
|
||||||
|
|
||||||
async getServerLogs(
|
async getServerLogs(
|
||||||
params: RR.GetServerLogsReq,
|
params: RR.GetServerLogsReq,
|
||||||
): Promise<RR.GetServerLogsRes> {
|
): Promise<RR.GetServerLogsRes> {
|
||||||
return this.http.rpcRequest({ method: 'server.logs', params })
|
return this.rpcRequest({ method: 'server.logs', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async getKernelLogs(
|
async getKernelLogs(
|
||||||
params: RR.GetServerLogsReq,
|
params: RR.GetServerLogsReq,
|
||||||
): Promise<RR.GetServerLogsRes> {
|
): Promise<RR.GetServerLogsRes> {
|
||||||
return this.http.rpcRequest({ method: 'server.kernel-logs', params })
|
return this.rpcRequest({ method: 'server.kernel-logs', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async followServerLogs(
|
async followServerLogs(
|
||||||
params: RR.FollowServerLogsReq,
|
params: RR.FollowServerLogsReq,
|
||||||
): Promise<RR.FollowServerLogsRes> {
|
): Promise<RR.FollowServerLogsRes> {
|
||||||
return this.http.rpcRequest({ method: 'server.logs.follow', params })
|
return this.rpcRequest({ method: 'server.logs.follow', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async followKernelLogs(
|
async followKernelLogs(
|
||||||
params: RR.FollowServerLogsReq,
|
params: RR.FollowServerLogsReq,
|
||||||
): Promise<RR.FollowServerLogsRes> {
|
): Promise<RR.FollowServerLogsRes> {
|
||||||
return this.http.rpcRequest({ method: 'server.kernel-logs.follow', params })
|
return this.rpcRequest({ method: 'server.kernel-logs.follow', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async getServerMetrics(
|
async getServerMetrics(
|
||||||
params: RR.GetServerMetricsReq,
|
params: RR.GetServerMetricsReq,
|
||||||
): Promise<RR.GetServerMetricsRes> {
|
): Promise<RR.GetServerMetricsRes> {
|
||||||
return this.http.rpcRequest({ method: 'server.metrics', params })
|
return this.rpcRequest({ method: 'server.metrics', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateServerRaw(
|
async updateServerRaw(
|
||||||
params: RR.UpdateServerReq,
|
params: RR.UpdateServerReq,
|
||||||
): Promise<RR.UpdateServerRes> {
|
): Promise<RR.UpdateServerRes> {
|
||||||
return this.http.rpcRequest({ method: 'server.update', params })
|
return this.rpcRequest({ method: 'server.update', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async restartServer(
|
async restartServer(
|
||||||
params: RR.RestartServerReq,
|
params: RR.RestartServerReq,
|
||||||
): Promise<RR.RestartServerRes> {
|
): Promise<RR.RestartServerRes> {
|
||||||
return this.http.rpcRequest({ method: 'server.restart', params })
|
return this.rpcRequest({ method: 'server.restart', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async shutdownServer(
|
async shutdownServer(
|
||||||
params: RR.ShutdownServerReq,
|
params: RR.ShutdownServerReq,
|
||||||
): Promise<RR.ShutdownServerRes> {
|
): Promise<RR.ShutdownServerRes> {
|
||||||
return this.http.rpcRequest({ method: 'server.shutdown', params })
|
return this.rpcRequest({ method: 'server.shutdown', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async systemRebuild(
|
async systemRebuild(
|
||||||
params: RR.RestartServerReq,
|
params: RR.RestartServerReq,
|
||||||
): Promise<RR.RestartServerRes> {
|
): Promise<RR.RestartServerRes> {
|
||||||
return this.http.rpcRequest({ method: 'server.rebuild', params })
|
return this.rpcRequest({ method: 'server.rebuild', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async repairDisk(params: RR.RestartServerReq): Promise<RR.RestartServerRes> {
|
async repairDisk(params: RR.RestartServerReq): Promise<RR.RestartServerRes> {
|
||||||
return this.http.rpcRequest({ method: 'disk.repair', params })
|
return this.rpcRequest({ method: 'disk.repair', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
// marketplace URLs
|
// marketplace URLs
|
||||||
@@ -135,7 +164,7 @@ export class LiveApiService extends ApiService {
|
|||||||
async marketplaceProxy<T>(path: string, qp: {}, url: string): Promise<T> {
|
async marketplaceProxy<T>(path: string, qp: {}, url: string): Promise<T> {
|
||||||
Object.assign(qp, { arch: this.config.targetArch })
|
Object.assign(qp, { arch: this.config.targetArch })
|
||||||
const fullURL = `${url}${path}?${new URLSearchParams(qp).toString()}`
|
const fullURL = `${url}${path}?${new URLSearchParams(qp).toString()}`
|
||||||
return this.http.rpcRequest({
|
return this.rpcRequest({
|
||||||
method: 'marketplace.get',
|
method: 'marketplace.get',
|
||||||
params: { url: fullURL },
|
params: { url: fullURL },
|
||||||
})
|
})
|
||||||
@@ -156,19 +185,19 @@ export class LiveApiService extends ApiService {
|
|||||||
async getNotificationsRaw(
|
async getNotificationsRaw(
|
||||||
params: RR.GetNotificationsReq,
|
params: RR.GetNotificationsReq,
|
||||||
): Promise<RR.GetNotificationsRes> {
|
): Promise<RR.GetNotificationsRes> {
|
||||||
return this.http.rpcRequest({ method: 'notification.list', params })
|
return this.rpcRequest({ method: 'notification.list', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteNotification(
|
async deleteNotification(
|
||||||
params: RR.DeleteNotificationReq,
|
params: RR.DeleteNotificationReq,
|
||||||
): Promise<RR.DeleteNotificationRes> {
|
): Promise<RR.DeleteNotificationRes> {
|
||||||
return this.http.rpcRequest({ method: 'notification.delete', params })
|
return this.rpcRequest({ method: 'notification.delete', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteAllNotifications(
|
async deleteAllNotifications(
|
||||||
params: RR.DeleteAllNotificationsReq,
|
params: RR.DeleteAllNotificationsReq,
|
||||||
): Promise<RR.DeleteAllNotificationsRes> {
|
): Promise<RR.DeleteAllNotificationsRes> {
|
||||||
return this.http.rpcRequest({
|
return this.rpcRequest({
|
||||||
method: 'notification.delete-before',
|
method: 'notification.delete-before',
|
||||||
params,
|
params,
|
||||||
})
|
})
|
||||||
@@ -180,39 +209,39 @@ export class LiveApiService extends ApiService {
|
|||||||
params: RR.GetWifiReq,
|
params: RR.GetWifiReq,
|
||||||
timeout?: number,
|
timeout?: number,
|
||||||
): Promise<RR.GetWifiRes> {
|
): Promise<RR.GetWifiRes> {
|
||||||
return this.http.rpcRequest({ method: 'wifi.get', params, timeout })
|
return this.rpcRequest({ method: 'wifi.get', params, timeout })
|
||||||
}
|
}
|
||||||
|
|
||||||
async setWifiCountry(
|
async setWifiCountry(
|
||||||
params: RR.SetWifiCountryReq,
|
params: RR.SetWifiCountryReq,
|
||||||
): Promise<RR.SetWifiCountryRes> {
|
): Promise<RR.SetWifiCountryRes> {
|
||||||
return this.http.rpcRequest({ method: 'wifi.country.set', params })
|
return this.rpcRequest({ method: 'wifi.country.set', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async addWifi(params: RR.AddWifiReq): Promise<RR.AddWifiRes> {
|
async addWifi(params: RR.AddWifiReq): Promise<RR.AddWifiRes> {
|
||||||
return this.http.rpcRequest({ method: 'wifi.add', params })
|
return this.rpcRequest({ method: 'wifi.add', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async connectWifi(params: RR.ConnectWifiReq): Promise<RR.ConnectWifiRes> {
|
async connectWifi(params: RR.ConnectWifiReq): Promise<RR.ConnectWifiRes> {
|
||||||
return this.http.rpcRequest({ method: 'wifi.connect', params })
|
return this.rpcRequest({ method: 'wifi.connect', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteWifi(params: RR.DeleteWifiReq): Promise<RR.DeleteWifiRes> {
|
async deleteWifi(params: RR.DeleteWifiReq): Promise<RR.DeleteWifiRes> {
|
||||||
return this.http.rpcRequest({ method: 'wifi.delete', params })
|
return this.rpcRequest({ method: 'wifi.delete', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
// ssh
|
// ssh
|
||||||
|
|
||||||
async getSshKeys(params: RR.GetSSHKeysReq): Promise<RR.GetSSHKeysRes> {
|
async getSshKeys(params: RR.GetSSHKeysReq): Promise<RR.GetSSHKeysRes> {
|
||||||
return this.http.rpcRequest({ method: 'ssh.list', params })
|
return this.rpcRequest({ method: 'ssh.list', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async addSshKey(params: RR.AddSSHKeyReq): Promise<RR.AddSSHKeyRes> {
|
async addSshKey(params: RR.AddSSHKeyReq): Promise<RR.AddSSHKeyRes> {
|
||||||
return this.http.rpcRequest({ method: 'ssh.add', params })
|
return this.rpcRequest({ method: 'ssh.add', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteSshKey(params: RR.DeleteSSHKeyReq): Promise<RR.DeleteSSHKeyRes> {
|
async deleteSshKey(params: RR.DeleteSSHKeyReq): Promise<RR.DeleteSSHKeyRes> {
|
||||||
return this.http.rpcRequest({ method: 'ssh.delete', params })
|
return this.rpcRequest({ method: 'ssh.delete', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
// backup
|
// backup
|
||||||
@@ -220,38 +249,38 @@ export class LiveApiService extends ApiService {
|
|||||||
async getBackupTargets(
|
async getBackupTargets(
|
||||||
params: RR.GetBackupTargetsReq,
|
params: RR.GetBackupTargetsReq,
|
||||||
): Promise<RR.GetBackupTargetsRes> {
|
): Promise<RR.GetBackupTargetsRes> {
|
||||||
return this.http.rpcRequest({ method: 'backup.target.list', params })
|
return this.rpcRequest({ method: 'backup.target.list', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async addBackupTarget(
|
async addBackupTarget(
|
||||||
params: RR.AddBackupTargetReq,
|
params: RR.AddBackupTargetReq,
|
||||||
): Promise<RR.AddBackupTargetRes> {
|
): Promise<RR.AddBackupTargetRes> {
|
||||||
params.path = params.path.replace('/\\/g', '/')
|
params.path = params.path.replace('/\\/g', '/')
|
||||||
return this.http.rpcRequest({ method: 'backup.target.cifs.add', params })
|
return this.rpcRequest({ method: 'backup.target.cifs.add', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateBackupTarget(
|
async updateBackupTarget(
|
||||||
params: RR.UpdateBackupTargetReq,
|
params: RR.UpdateBackupTargetReq,
|
||||||
): Promise<RR.UpdateBackupTargetRes> {
|
): Promise<RR.UpdateBackupTargetRes> {
|
||||||
return this.http.rpcRequest({ method: 'backup.target.cifs.update', params })
|
return this.rpcRequest({ method: 'backup.target.cifs.update', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeBackupTarget(
|
async removeBackupTarget(
|
||||||
params: RR.RemoveBackupTargetReq,
|
params: RR.RemoveBackupTargetReq,
|
||||||
): Promise<RR.RemoveBackupTargetRes> {
|
): Promise<RR.RemoveBackupTargetRes> {
|
||||||
return this.http.rpcRequest({ method: 'backup.target.cifs.remove', params })
|
return this.rpcRequest({ method: 'backup.target.cifs.remove', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async getBackupInfo(
|
async getBackupInfo(
|
||||||
params: RR.GetBackupInfoReq,
|
params: RR.GetBackupInfoReq,
|
||||||
): Promise<RR.GetBackupInfoRes> {
|
): Promise<RR.GetBackupInfoRes> {
|
||||||
return this.http.rpcRequest({ method: 'backup.target.info', params })
|
return this.rpcRequest({ method: 'backup.target.info', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async createBackupRaw(
|
async createBackupRaw(
|
||||||
params: RR.CreateBackupReq,
|
params: RR.CreateBackupReq,
|
||||||
): Promise<RR.CreateBackupRes> {
|
): Promise<RR.CreateBackupRes> {
|
||||||
return this.http.rpcRequest({ method: 'backup.create', params })
|
return this.rpcRequest({ method: 'backup.create', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
// package
|
// package
|
||||||
@@ -267,95 +296,95 @@ export class LiveApiService extends ApiService {
|
|||||||
async getPackageLogs(
|
async getPackageLogs(
|
||||||
params: RR.GetPackageLogsReq,
|
params: RR.GetPackageLogsReq,
|
||||||
): Promise<RR.GetPackageLogsRes> {
|
): Promise<RR.GetPackageLogsRes> {
|
||||||
return this.http.rpcRequest({ method: 'package.logs', params })
|
return this.rpcRequest({ method: 'package.logs', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async followPackageLogs(
|
async followPackageLogs(
|
||||||
params: RR.FollowServerLogsReq,
|
params: RR.FollowServerLogsReq,
|
||||||
): Promise<RR.FollowServerLogsRes> {
|
): Promise<RR.FollowServerLogsRes> {
|
||||||
return this.http.rpcRequest({ method: 'package.logs.follow', params })
|
return this.rpcRequest({ method: 'package.logs.follow', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPkgMetrics(
|
async getPkgMetrics(
|
||||||
params: RR.GetPackageMetricsReq,
|
params: RR.GetPackageMetricsReq,
|
||||||
): Promise<RR.GetPackageMetricsRes> {
|
): Promise<RR.GetPackageMetricsRes> {
|
||||||
return this.http.rpcRequest({ method: 'package.metrics', params })
|
return this.rpcRequest({ method: 'package.metrics', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async installPackageRaw(
|
async installPackageRaw(
|
||||||
params: RR.InstallPackageReq,
|
params: RR.InstallPackageReq,
|
||||||
): Promise<RR.InstallPackageRes> {
|
): Promise<RR.InstallPackageRes> {
|
||||||
return this.http.rpcRequest({ method: 'package.install', params })
|
return this.rpcRequest({ method: 'package.install', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async dryUpdatePackage(
|
async dryUpdatePackage(
|
||||||
params: RR.DryUpdatePackageReq,
|
params: RR.DryUpdatePackageReq,
|
||||||
): Promise<RR.DryUpdatePackageRes> {
|
): Promise<RR.DryUpdatePackageRes> {
|
||||||
return this.http.rpcRequest({ method: 'package.update.dry', params })
|
return this.rpcRequest({ method: 'package.update.dry', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPackageConfig(
|
async getPackageConfig(
|
||||||
params: RR.GetPackageConfigReq,
|
params: RR.GetPackageConfigReq,
|
||||||
): Promise<RR.GetPackageConfigRes> {
|
): Promise<RR.GetPackageConfigRes> {
|
||||||
return this.http.rpcRequest({ method: 'package.config.get', params })
|
return this.rpcRequest({ method: 'package.config.get', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async drySetPackageConfig(
|
async drySetPackageConfig(
|
||||||
params: RR.DrySetPackageConfigReq,
|
params: RR.DrySetPackageConfigReq,
|
||||||
): Promise<RR.DrySetPackageConfigRes> {
|
): Promise<RR.DrySetPackageConfigRes> {
|
||||||
return this.http.rpcRequest({ method: 'package.config.set.dry', params })
|
return this.rpcRequest({ method: 'package.config.set.dry', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async setPackageConfigRaw(
|
async setPackageConfigRaw(
|
||||||
params: RR.SetPackageConfigReq,
|
params: RR.SetPackageConfigReq,
|
||||||
): Promise<RR.SetPackageConfigRes> {
|
): Promise<RR.SetPackageConfigRes> {
|
||||||
return this.http.rpcRequest({ method: 'package.config.set', params })
|
return this.rpcRequest({ method: 'package.config.set', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async restorePackagesRaw(
|
async restorePackagesRaw(
|
||||||
params: RR.RestorePackagesReq,
|
params: RR.RestorePackagesReq,
|
||||||
): Promise<RR.RestorePackagesRes> {
|
): Promise<RR.RestorePackagesRes> {
|
||||||
return this.http.rpcRequest({ method: 'package.backup.restore', params })
|
return this.rpcRequest({ method: 'package.backup.restore', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async executePackageAction(
|
async executePackageAction(
|
||||||
params: RR.ExecutePackageActionReq,
|
params: RR.ExecutePackageActionReq,
|
||||||
): Promise<RR.ExecutePackageActionRes> {
|
): Promise<RR.ExecutePackageActionRes> {
|
||||||
return this.http.rpcRequest({ method: 'package.action', params })
|
return this.rpcRequest({ method: 'package.action', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async startPackageRaw(
|
async startPackageRaw(
|
||||||
params: RR.StartPackageReq,
|
params: RR.StartPackageReq,
|
||||||
): Promise<RR.StartPackageRes> {
|
): Promise<RR.StartPackageRes> {
|
||||||
return this.http.rpcRequest({ method: 'package.start', params })
|
return this.rpcRequest({ method: 'package.start', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async restartPackageRaw(
|
async restartPackageRaw(
|
||||||
params: RR.RestartPackageReq,
|
params: RR.RestartPackageReq,
|
||||||
): Promise<RR.RestartPackageRes> {
|
): Promise<RR.RestartPackageRes> {
|
||||||
return this.http.rpcRequest({ method: 'package.restart', params })
|
return this.rpcRequest({ method: 'package.restart', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async stopPackageRaw(params: RR.StopPackageReq): Promise<RR.StopPackageRes> {
|
async stopPackageRaw(params: RR.StopPackageReq): Promise<RR.StopPackageRes> {
|
||||||
return this.http.rpcRequest({ method: 'package.stop', params })
|
return this.rpcRequest({ method: 'package.stop', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteRecoveredPackageRaw(
|
async deleteRecoveredPackageRaw(
|
||||||
params: RR.DeleteRecoveredPackageReq,
|
params: RR.DeleteRecoveredPackageReq,
|
||||||
): Promise<RR.DeleteRecoveredPackageRes> {
|
): Promise<RR.DeleteRecoveredPackageRes> {
|
||||||
return this.http.rpcRequest({ method: 'package.delete-recovered', params })
|
return this.rpcRequest({ method: 'package.delete-recovered', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async uninstallPackageRaw(
|
async uninstallPackageRaw(
|
||||||
params: RR.UninstallPackageReq,
|
params: RR.UninstallPackageReq,
|
||||||
): Promise<RR.UninstallPackageRes> {
|
): Promise<RR.UninstallPackageRes> {
|
||||||
return this.http.rpcRequest({ method: 'package.uninstall', params })
|
return this.rpcRequest({ method: 'package.uninstall', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async dryConfigureDependency(
|
async dryConfigureDependency(
|
||||||
params: RR.DryConfigureDependencyReq,
|
params: RR.DryConfigureDependencyReq,
|
||||||
): Promise<RR.DryConfigureDependencyRes> {
|
): Promise<RR.DryConfigureDependencyRes> {
|
||||||
return this.http.rpcRequest({
|
return this.rpcRequest({
|
||||||
method: 'package.dependency.configure.dry',
|
method: 'package.dependency.configure.dry',
|
||||||
params,
|
params,
|
||||||
})
|
})
|
||||||
@@ -364,9 +393,29 @@ export class LiveApiService extends ApiService {
|
|||||||
async sideloadPackage(
|
async sideloadPackage(
|
||||||
params: RR.SideloadPackageReq,
|
params: RR.SideloadPackageReq,
|
||||||
): Promise<RR.SideloadPacakgeRes> {
|
): Promise<RR.SideloadPacakgeRes> {
|
||||||
return this.http.rpcRequest({
|
return this.rpcRequest({
|
||||||
method: 'package.sideload',
|
method: 'package.sideload',
|
||||||
params,
|
params,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private openWebsocket<T>(config: WebSocketSubjectConfig<T>): Observable<T> {
|
||||||
|
const { location } = this.document.defaultView!
|
||||||
|
const protocol = location.protocol === 'http:' ? 'ws' : 'wss'
|
||||||
|
const host = location.host
|
||||||
|
|
||||||
|
config.url = `${protocol}://${host}/ws${config.url}`
|
||||||
|
|
||||||
|
return webSocket(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async rpcRequest<T>(options: RPCOptions): Promise<T> {
|
||||||
|
return this.http.rpcRequest<T>(options).catch(e => {
|
||||||
|
if ((e as RPCError).error.code === 34) {
|
||||||
|
console.error('Unauthenticated, logging out')
|
||||||
|
this.auth.setUnverified()
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,16 +44,6 @@ export class MockApiService extends ApiService {
|
|||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
|
|
||||||
openLogsWebsocket$(config: WebSocketSubjectConfig<Log>): Observable<Log> {
|
|
||||||
return interval(100).pipe(
|
|
||||||
map((_, index) => {
|
|
||||||
// mock fire open observer
|
|
||||||
if (index === 0) config.openObserver?.next(new Event(''))
|
|
||||||
return Mock.ServerLogs[0]
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async getStatic(url: string): Promise<string> {
|
async getStatic(url: string): Promise<string> {
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
return markdown
|
return markdown
|
||||||
@@ -120,6 +110,25 @@ export class MockApiService extends ApiService {
|
|||||||
|
|
||||||
// server
|
// server
|
||||||
|
|
||||||
|
async echo(params: RR.EchoReq): Promise<RR.EchoRes> {
|
||||||
|
await pauseFor(2000)
|
||||||
|
return params.message
|
||||||
|
}
|
||||||
|
|
||||||
|
openPatchWebsocket$(): Observable<Update<DataModel>> {
|
||||||
|
return this.mockPatch$
|
||||||
|
}
|
||||||
|
|
||||||
|
openLogsWebsocket$(config: WebSocketSubjectConfig<Log>): Observable<Log> {
|
||||||
|
return interval(100).pipe(
|
||||||
|
map((_, index) => {
|
||||||
|
// mock fire open observer
|
||||||
|
if (index === 0) config.openObserver?.next(new Event(''))
|
||||||
|
return Mock.ServerLogs[0]
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async getServerLogs(
|
async getServerLogs(
|
||||||
params: RR.GetServerLogsReq,
|
params: RR.GetServerLogsReq,
|
||||||
): Promise<RR.GetServerLogsRes> {
|
): Promise<RR.GetServerLogsRes> {
|
||||||
@@ -291,7 +300,6 @@ export class MockApiService extends ApiService {
|
|||||||
value: 0,
|
value: 0,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
return this.withRevision(patch, Mock.Notifications)
|
return this.withRevision(patch, Mock.Notifications)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable, NgZone } from '@angular/core'
|
||||||
import { Observable, ReplaySubject } from 'rxjs'
|
import { ReplaySubject } from 'rxjs'
|
||||||
import { distinctUntilChanged, map } from 'rxjs/operators'
|
import { map } from 'rxjs/operators'
|
||||||
import { Storage } from '@ionic/storage-angular'
|
import { Storage } from '@ionic/storage-angular'
|
||||||
|
import { Router } from '@angular/router'
|
||||||
|
|
||||||
export enum AuthState {
|
export enum AuthState {
|
||||||
UNVERIFIED,
|
UNVERIFIED,
|
||||||
@@ -14,19 +15,23 @@ export class AuthService {
|
|||||||
private readonly LOGGED_IN_KEY = 'loggedInKey'
|
private readonly LOGGED_IN_KEY = 'loggedInKey'
|
||||||
private readonly authState$ = new ReplaySubject<AuthState>(1)
|
private readonly authState$ = new ReplaySubject<AuthState>(1)
|
||||||
|
|
||||||
readonly isVerified$ = this.watch$().pipe(
|
readonly isVerified$ = this.authState$.pipe(
|
||||||
map(state => state === AuthState.VERIFIED),
|
map(state => state === AuthState.VERIFIED),
|
||||||
)
|
)
|
||||||
|
|
||||||
constructor(private readonly storage: Storage) {}
|
constructor(
|
||||||
|
private readonly storage: Storage,
|
||||||
|
private readonly zone: NgZone,
|
||||||
|
private readonly router: Router,
|
||||||
|
) {}
|
||||||
|
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
const loggedIn = await this.storage.get(this.LOGGED_IN_KEY)
|
const loggedIn = await this.storage.get(this.LOGGED_IN_KEY)
|
||||||
this.authState$.next(loggedIn ? AuthState.VERIFIED : AuthState.UNVERIFIED)
|
if (loggedIn) {
|
||||||
}
|
this.setVerified()
|
||||||
|
} else {
|
||||||
watch$(): Observable<AuthState> {
|
this.setUnverified()
|
||||||
return this.authState$.pipe(distinctUntilChanged())
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setVerified(): Promise<void> {
|
async setVerified(): Promise<void> {
|
||||||
@@ -34,7 +39,11 @@ export class AuthService {
|
|||||||
this.authState$.next(AuthState.VERIFIED)
|
this.authState$.next(AuthState.VERIFIED)
|
||||||
}
|
}
|
||||||
|
|
||||||
async setUnverified(): Promise<void> {
|
setUnverified(): void {
|
||||||
this.authState$.next(AuthState.UNVERIFIED)
|
this.authState$.next(AuthState.UNVERIFIED)
|
||||||
|
this.storage.clear()
|
||||||
|
this.zone.run(() => {
|
||||||
|
this.router.navigate(['/login'], { replaceUrl: true })
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const {
|
|||||||
targetArch,
|
targetArch,
|
||||||
gitHash,
|
gitHash,
|
||||||
useMocks,
|
useMocks,
|
||||||
ui: { patchDb, api, mocks, marketplace },
|
ui: { api, mocks, marketplace },
|
||||||
} = require('../../../../../config.json') as WorkspaceConfig
|
} = require('../../../../../config.json') as WorkspaceConfig
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@@ -24,7 +24,6 @@ export class ConfigService {
|
|||||||
mocks = mocks
|
mocks = mocks
|
||||||
targetArch = targetArch
|
targetArch = targetArch
|
||||||
gitHash = gitHash
|
gitHash = gitHash
|
||||||
patchDb = patchDb
|
|
||||||
api = api
|
api = api
|
||||||
marketplace = marketplace
|
marketplace = marketplace
|
||||||
skipStartupAlerts = useMocks && mocks.skipStartupAlerts
|
skipStartupAlerts = useMocks && mocks.skipStartupAlerts
|
||||||
|
|||||||
@@ -1,83 +1,22 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import {
|
import { combineLatest, fromEvent, merge, ReplaySubject } from 'rxjs'
|
||||||
BehaviorSubject,
|
import { distinctUntilChanged, map, startWith } from 'rxjs/operators'
|
||||||
combineLatest,
|
|
||||||
fromEvent,
|
|
||||||
merge,
|
|
||||||
Observable,
|
|
||||||
} from 'rxjs'
|
|
||||||
import { PatchConnection, PatchDbService } from './patch-db/patch-db.service'
|
|
||||||
import {
|
|
||||||
distinctUntilChanged,
|
|
||||||
map,
|
|
||||||
mapTo,
|
|
||||||
startWith,
|
|
||||||
tap,
|
|
||||||
} from 'rxjs/operators'
|
|
||||||
import { ConfigService } from './config.service'
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class ConnectionService {
|
export class ConnectionService {
|
||||||
private readonly networkState$ = merge(
|
readonly networkConnected$ = merge(
|
||||||
fromEvent(window, 'online').pipe(mapTo(true)),
|
fromEvent(window, 'online'),
|
||||||
fromEvent(window, 'offline').pipe(mapTo(false)),
|
fromEvent(window, 'offline'),
|
||||||
).pipe(
|
).pipe(
|
||||||
startWith(null),
|
startWith(null),
|
||||||
map(() => navigator.onLine),
|
map(() => navigator.onLine),
|
||||||
|
distinctUntilChanged(),
|
||||||
)
|
)
|
||||||
|
readonly websocketConnected$ = new ReplaySubject<boolean>(1)
|
||||||
private readonly connectionFailure$ = new BehaviorSubject<ConnectionFailure>(
|
readonly connected$ = combineLatest([
|
||||||
ConnectionFailure.None,
|
this.networkConnected$,
|
||||||
)
|
this.websocketConnected$,
|
||||||
|
]).pipe(map(([network, websocket]) => network && websocket))
|
||||||
constructor(
|
|
||||||
private readonly configService: ConfigService,
|
|
||||||
private readonly patch: PatchDbService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
watchFailure$() {
|
|
||||||
return this.connectionFailure$.asObservable()
|
|
||||||
}
|
|
||||||
|
|
||||||
watchDisconnected$() {
|
|
||||||
return this.connectionFailure$.pipe(
|
|
||||||
map(failure => failure !== ConnectionFailure.None),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
start(): Observable<unknown> {
|
|
||||||
return combineLatest([
|
|
||||||
// 1
|
|
||||||
this.networkState$.pipe(distinctUntilChanged()),
|
|
||||||
// 2
|
|
||||||
this.patch.watchPatchConnection$().pipe(distinctUntilChanged()),
|
|
||||||
// 3
|
|
||||||
this.patch
|
|
||||||
.watch$('server-info', 'status-info', 'update-progress')
|
|
||||||
.pipe(distinctUntilChanged()),
|
|
||||||
]).pipe(
|
|
||||||
tap(([network, patchConnection, progress]) => {
|
|
||||||
if (!network) {
|
|
||||||
this.connectionFailure$.next(ConnectionFailure.Network)
|
|
||||||
} else if (patchConnection !== PatchConnection.Disconnected) {
|
|
||||||
this.connectionFailure$.next(ConnectionFailure.None)
|
|
||||||
} else if (!!progress && progress.downloaded === progress.size) {
|
|
||||||
this.connectionFailure$.next(ConnectionFailure.None)
|
|
||||||
} else if (!this.configService.isTor()) {
|
|
||||||
this.connectionFailure$.next(ConnectionFailure.Lan)
|
|
||||||
} else {
|
|
||||||
this.connectionFailure$.next(ConnectionFailure.Tor)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ConnectionFailure {
|
|
||||||
None = 'none',
|
|
||||||
Network = 'network',
|
|
||||||
Tor = 'tor',
|
|
||||||
Lan = 'lan',
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { Emver } from '@start9labs/shared'
|
import { Emver } from '@start9labs/shared'
|
||||||
import { BehaviorSubject, combineLatest } from 'rxjs'
|
import { BehaviorSubject, combineLatest } from 'rxjs'
|
||||||
import { distinctUntilChanged, map } from 'rxjs/operators'
|
import { distinctUntilChanged, filter, map } from 'rxjs/operators'
|
||||||
|
|
||||||
import { MarketplaceEOS } from 'src/app/services/api/api.types'
|
import { MarketplaceEOS } from 'src/app/services/api/api.types'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
@@ -16,6 +16,7 @@ export class EOSService {
|
|||||||
updateAvailable$ = new BehaviorSubject<boolean>(false)
|
updateAvailable$ = new BehaviorSubject<boolean>(false)
|
||||||
|
|
||||||
readonly updating$ = this.patch.watch$('server-info', 'status-info').pipe(
|
readonly updating$ = this.patch.watch$('server-info', 'status-info').pipe(
|
||||||
|
filter(Boolean),
|
||||||
map(status => !!status['update-progress'] || status.updated),
|
map(status => !!status['update-progress'] || status.updated),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import { ErrorHandler, Injectable } from '@angular/core'
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class GlobalErrorHandler implements ErrorHandler {
|
|
||||||
|
|
||||||
handleError (e: any): void {
|
|
||||||
console.error(e)
|
|
||||||
const chunkFailedMessage = /Loading chunk [\d]+ failed/
|
|
||||||
|
|
||||||
if (chunkFailedMessage.test(e.message)) {
|
|
||||||
window.location.reload()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||||
import {
|
import {
|
||||||
catchError,
|
catchError,
|
||||||
|
distinctUntilChanged,
|
||||||
filter,
|
filter,
|
||||||
map,
|
map,
|
||||||
shareReplay,
|
shareReplay,
|
||||||
@@ -32,9 +33,14 @@ export class MarketplaceService extends AbstractMarketplaceService {
|
|||||||
private readonly notes = new Map<string, Record<string, string>>()
|
private readonly notes = new Map<string, Record<string, string>>()
|
||||||
private readonly hasPackages$ = new Subject<boolean>()
|
private readonly hasPackages$ = new Subject<boolean>()
|
||||||
|
|
||||||
private readonly uiMarketplaceData$: Observable<
|
private readonly uiMarketplaceData$: Observable<UIMarketplaceData> =
|
||||||
UIMarketplaceData | undefined
|
this.patch.watch$('ui', 'marketplace').pipe(
|
||||||
> = this.patch.watch$('ui', 'marketplace').pipe(shareReplay(1))
|
filter(Boolean),
|
||||||
|
distinctUntilChanged(
|
||||||
|
(prev, curr) => prev['selected-id'] === curr['selected-id'],
|
||||||
|
),
|
||||||
|
shareReplay(1),
|
||||||
|
)
|
||||||
|
|
||||||
private readonly marketplace$ = this.uiMarketplaceData$.pipe(
|
private readonly marketplace$ = this.uiMarketplaceData$.pipe(
|
||||||
map(data => this.toMarketplace(data)),
|
map(data => this.toMarketplace(data)),
|
||||||
@@ -42,19 +48,19 @@ export class MarketplaceService extends AbstractMarketplaceService {
|
|||||||
|
|
||||||
private readonly serverInfo$: Observable<ServerInfo> = this.patch
|
private readonly serverInfo$: Observable<ServerInfo> = this.patch
|
||||||
.watch$('server-info')
|
.watch$('server-info')
|
||||||
.pipe(take(1), shareReplay())
|
.pipe(filter(Boolean), take(1), shareReplay())
|
||||||
|
|
||||||
private readonly registryData$: Observable<MarketplaceData> =
|
private readonly registryData$: Observable<MarketplaceData> =
|
||||||
this.uiMarketplaceData$.pipe(
|
this.uiMarketplaceData$.pipe(
|
||||||
switchMap(uiMarketplaceData =>
|
switchMap(data =>
|
||||||
this.serverInfo$.pipe(
|
this.serverInfo$.pipe(
|
||||||
switchMap(({ id }) =>
|
switchMap(({ id }) =>
|
||||||
from(
|
from(
|
||||||
this.getMarketplaceData(
|
this.getMarketplaceData(
|
||||||
{ 'server-id': id },
|
{ 'server-id': id },
|
||||||
this.toMarketplace(uiMarketplaceData).url,
|
this.toMarketplace(data).url,
|
||||||
),
|
),
|
||||||
).pipe(tap(({ name }) => this.updateName(uiMarketplaceData, name))),
|
).pipe(tap(({ name }) => this.updateName(data, name))),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -126,7 +132,7 @@ export class MarketplaceService extends AbstractMarketplaceService {
|
|||||||
return this.marketplace$
|
return this.marketplace$
|
||||||
}
|
}
|
||||||
|
|
||||||
getAltMarketplace(): Observable<UIMarketplaceData | undefined> {
|
getAltMarketplaceData(): Observable<UIMarketplaceData> {
|
||||||
return this.uiMarketplaceData$
|
return this.uiMarketplaceData$
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,44 +1,38 @@
|
|||||||
import { Inject, Injectable } from '@angular/core'
|
import { Inject, Injectable } from '@angular/core'
|
||||||
import { ModalController } from '@ionic/angular'
|
import { ModalController } from '@ionic/angular'
|
||||||
import { Observable, of } from 'rxjs'
|
import { Observable } from 'rxjs'
|
||||||
import { filter, share, switchMap, take, tap } from 'rxjs/operators'
|
import { filter, share, switchMap, take, tap } from 'rxjs/operators'
|
||||||
import { isEmptyObject } from '@start9labs/shared'
|
import { exists, isEmptyObject } from '@start9labs/shared'
|
||||||
|
|
||||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||||
import { DataModel, UIData } from 'src/app/services/patch-db/data-model'
|
import { DataModel, UIData } from 'src/app/services/patch-db/data-model'
|
||||||
import { EOSService } from 'src/app/services/eos.service'
|
import { EOSService } from 'src/app/services/eos.service'
|
||||||
import { OSWelcomePage } from 'src/app/modals/os-welcome/os-welcome.page'
|
import { OSWelcomePage } from 'src/app/modals/os-welcome/os-welcome.page'
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
import { ConfigService } from 'src/app/services/config.service'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { PatchMonitorService } from './patch-monitor.service'
|
|
||||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||||
import { AbstractMarketplaceService } from '../../../../../../marketplace/src/services/marketplace.service'
|
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
||||||
|
import { ConnectionService } from 'src/app/services/connection.service'
|
||||||
|
|
||||||
// Get data from PatchDb after is starts and act upon it
|
// Get data from PatchDb after is starts and act upon it
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class PatchDataService extends Observable<DataModel | null> {
|
export class PatchDataService extends Observable<DataModel> {
|
||||||
private readonly stream$ = this.patchMonitor.pipe(
|
private readonly stream$ = this.connectionService.connected$.pipe(
|
||||||
switchMap(started =>
|
filter(Boolean),
|
||||||
started
|
switchMap(() => this.patch.watch$()),
|
||||||
? this.patch.watch$().pipe(
|
filter(obj => exists(obj) && !isEmptyObject(obj)),
|
||||||
filter(obj => !isEmptyObject(obj)),
|
take(1),
|
||||||
take(1),
|
tap(({ ui }) => {
|
||||||
tap(({ ui }) => {
|
// check for updates to EOS and services
|
||||||
// check for updates to EOS and services
|
this.checkForUpdates(ui)
|
||||||
this.checkForUpdates(ui)
|
// show eos welcome message
|
||||||
// show eos welcome message
|
this.showEosWelcome(ui['ack-welcome'])
|
||||||
this.showEosWelcome(ui['ack-welcome'])
|
}),
|
||||||
}),
|
|
||||||
)
|
|
||||||
: of(null),
|
|
||||||
),
|
|
||||||
share(),
|
share(),
|
||||||
)
|
)
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly patchMonitor: PatchMonitorService,
|
|
||||||
private readonly patch: PatchDbService,
|
private readonly patch: PatchDbService,
|
||||||
private readonly eosService: EOSService,
|
private readonly eosService: EOSService,
|
||||||
private readonly config: ConfigService,
|
private readonly config: ConfigService,
|
||||||
@@ -46,6 +40,7 @@ export class PatchDataService extends Observable<DataModel | null> {
|
|||||||
private readonly embassyApi: ApiService,
|
private readonly embassyApi: ApiService,
|
||||||
@Inject(AbstractMarketplaceService)
|
@Inject(AbstractMarketplaceService)
|
||||||
private readonly marketplaceService: MarketplaceService,
|
private readonly marketplaceService: MarketplaceService,
|
||||||
|
private readonly connectionService: ConnectionService,
|
||||||
) {
|
) {
|
||||||
super(subscriber => this.stream$.subscribe(subscriber))
|
super(subscriber => this.stream$.subscribe(subscriber))
|
||||||
}
|
}
|
||||||
@@ -41,8 +41,8 @@ export interface DevData {
|
|||||||
|
|
||||||
export interface DevProjectData {
|
export interface DevProjectData {
|
||||||
name: string
|
name: string
|
||||||
instructions?: string
|
instructions: string
|
||||||
config?: string
|
config: string
|
||||||
'basic-info'?: BasicInfo
|
'basic-info'?: BasicInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,50 +1,41 @@
|
|||||||
import { InjectionToken } from '@angular/core'
|
import { InjectionToken } from '@angular/core'
|
||||||
import { exists } from '@start9labs/shared'
|
import { catchError, switchMap, take, tap } from 'rxjs/operators'
|
||||||
import { filter } from 'rxjs/operators'
|
import { Bootstrapper, DBCache, Update } from 'patch-db-client'
|
||||||
import {
|
|
||||||
Bootstrapper,
|
|
||||||
DBCache,
|
|
||||||
MockSource,
|
|
||||||
PollSource,
|
|
||||||
Source,
|
|
||||||
WebsocketSource,
|
|
||||||
} from 'patch-db-client'
|
|
||||||
|
|
||||||
import { ConfigService } from '../config.service'
|
|
||||||
import { ApiService } from '../api/embassy-api.service'
|
|
||||||
import { MockApiService } from '../api/embassy-mock-api.service'
|
|
||||||
import { DataModel } from './data-model'
|
import { DataModel } from './data-model'
|
||||||
import { BehaviorSubject } from 'rxjs'
|
import { EMPTY, from, interval, merge, Observable } from 'rxjs'
|
||||||
|
import { AuthService } from '../auth.service'
|
||||||
|
import { ConnectionService } from '../connection.service'
|
||||||
|
import { ApiService } from '../api/embassy-api.service'
|
||||||
|
|
||||||
// [wsSources, pollSources]
|
export const PATCH_SOURCE = new InjectionToken<Observable<Update<DataModel>>>(
|
||||||
export const PATCH_SOURCE = new InjectionToken<Source<DataModel>[]>('')
|
'',
|
||||||
export const PATCH_SOURCE$ = new InjectionToken<
|
)
|
||||||
BehaviorSubject<Source<DataModel>[]>
|
|
||||||
>('')
|
|
||||||
export const PATCH_CACHE = new InjectionToken<DBCache<DataModel>>('', {
|
export const PATCH_CACHE = new InjectionToken<DBCache<DataModel>>('', {
|
||||||
factory: () => ({} as any),
|
factory: () => ({} as any),
|
||||||
})
|
})
|
||||||
export const BOOTSTRAPPER = new InjectionToken<Bootstrapper<DataModel>>('')
|
export const BOOTSTRAPPER = new InjectionToken<Bootstrapper<DataModel>>('')
|
||||||
|
|
||||||
export function mockSourceFactory({
|
export function sourceFactory(
|
||||||
mockPatch$,
|
api: ApiService,
|
||||||
}: MockApiService): Source<DataModel>[] {
|
authService: AuthService,
|
||||||
return Array(2).fill(
|
connectionService: ConnectionService,
|
||||||
new MockSource<DataModel>(mockPatch$.pipe(filter(exists))),
|
): Observable<Update<DataModel>> {
|
||||||
|
const websocket$ = api.openPatchWebsocket$().pipe(
|
||||||
|
catchError((_, watch$) => {
|
||||||
|
connectionService.websocketConnected$.next(false)
|
||||||
|
|
||||||
|
return interval(4000).pipe(
|
||||||
|
switchMap(() =>
|
||||||
|
from(api.echo({ message: 'ping' })).pipe(catchError(() => EMPTY)),
|
||||||
|
),
|
||||||
|
take(1),
|
||||||
|
switchMap(() => watch$),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
tap(() => connectionService.websocketConnected$.next(true)),
|
||||||
|
)
|
||||||
|
|
||||||
|
return authService.isVerified$.pipe(
|
||||||
|
switchMap(verified => (verified ? merge(websocket$, api.sync$) : EMPTY)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function realSourceFactory(
|
|
||||||
embassyApi: ApiService,
|
|
||||||
config: ConfigService,
|
|
||||||
{ defaultView }: Document,
|
|
||||||
): Source<DataModel>[] {
|
|
||||||
const { patchDb } = config
|
|
||||||
const host = defaultView?.location.host
|
|
||||||
const protocol = defaultView?.location.protocol === 'http:' ? 'ws' : 'wss'
|
|
||||||
|
|
||||||
return [
|
|
||||||
new WebsocketSource<DataModel>(`${protocol}://${host}/ws/db`),
|
|
||||||
new PollSource<DataModel>({ ...patchDb.poll }, embassyApi),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,22 +1,15 @@
|
|||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { DOCUMENT } from '@angular/common'
|
|
||||||
import { WorkspaceConfig } from '@start9labs/shared'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BOOTSTRAPPER,
|
BOOTSTRAPPER,
|
||||||
mockSourceFactory,
|
|
||||||
PATCH_CACHE,
|
PATCH_CACHE,
|
||||||
PATCH_SOURCE,
|
PATCH_SOURCE,
|
||||||
PATCH_SOURCE$,
|
sourceFactory,
|
||||||
realSourceFactory,
|
|
||||||
} from './patch-db.factory'
|
} from './patch-db.factory'
|
||||||
import { LocalStorageBootstrap } from './local-storage-bootstrap'
|
import { LocalStorageBootstrap } from './local-storage-bootstrap'
|
||||||
import { ApiService } from '../api/embassy-api.service'
|
import { ApiService } from '../api/embassy-api.service'
|
||||||
import { ConfigService } from '../config.service'
|
import { AuthService } from '../auth.service'
|
||||||
import { ReplaySubject } from 'rxjs'
|
import { ConnectionService } from '../connection.service'
|
||||||
|
|
||||||
const { useMocks } = require('../../../../../../config.json') as WorkspaceConfig
|
|
||||||
|
|
||||||
// This module is purely for providers organization purposes
|
// This module is purely for providers organization purposes
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@@ -27,16 +20,12 @@ const { useMocks } = require('../../../../../../config.json') as WorkspaceConfig
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: PATCH_SOURCE,
|
provide: PATCH_SOURCE,
|
||||||
deps: [ApiService, ConfigService, DOCUMENT],
|
deps: [ApiService, AuthService, ConnectionService],
|
||||||
useFactory: useMocks ? mockSourceFactory : realSourceFactory,
|
useFactory: sourceFactory,
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: PATCH_SOURCE$,
|
|
||||||
useValue: new ReplaySubject(1),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: PatchDB,
|
provide: PatchDB,
|
||||||
deps: [PATCH_SOURCE$, ApiService, PATCH_CACHE],
|
deps: [PATCH_SOURCE, PATCH_CACHE],
|
||||||
useClass: PatchDB,
|
useClass: PatchDB,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,165 +1,49 @@
|
|||||||
import { Inject, Injectable } from '@angular/core'
|
import { Inject, Injectable } from '@angular/core'
|
||||||
import { Storage } from '@ionic/storage-angular'
|
import { Bootstrapper, PatchDB, Store } from 'patch-db-client'
|
||||||
import { Bootstrapper, PatchDB, Source, Store } from 'patch-db-client'
|
import { Observable, of, Subscription } from 'rxjs'
|
||||||
import {
|
import { catchError, debounceTime, finalize, tap } from 'rxjs/operators'
|
||||||
BehaviorSubject,
|
|
||||||
Observable,
|
|
||||||
of,
|
|
||||||
ReplaySubject,
|
|
||||||
Subscription,
|
|
||||||
} from 'rxjs'
|
|
||||||
import {
|
|
||||||
catchError,
|
|
||||||
debounceTime,
|
|
||||||
filter,
|
|
||||||
finalize,
|
|
||||||
mergeMap,
|
|
||||||
shareReplay,
|
|
||||||
switchMap,
|
|
||||||
take,
|
|
||||||
tap,
|
|
||||||
withLatestFrom,
|
|
||||||
} from 'rxjs/operators'
|
|
||||||
import { pauseFor } from '@start9labs/shared'
|
|
||||||
import { DataModel } from './data-model'
|
import { DataModel } from './data-model'
|
||||||
import { ApiService } from '../api/embassy-api.service'
|
import { BOOTSTRAPPER } from './patch-db.factory'
|
||||||
import { AuthService } from '../auth.service'
|
|
||||||
import { BOOTSTRAPPER, PATCH_SOURCE, PATCH_SOURCE$ } from './patch-db.factory'
|
|
||||||
|
|
||||||
export enum PatchConnection {
|
|
||||||
Initializing = 'initializing',
|
|
||||||
Connected = 'connected',
|
|
||||||
Disconnected = 'disconnected',
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class PatchDbService {
|
export class PatchDbService {
|
||||||
private readonly WS_SUCCESS = 'wsSuccess'
|
private sub?: Subscription
|
||||||
private readonly patchConnection$ = new ReplaySubject<PatchConnection>(1)
|
|
||||||
private readonly wsSuccess$ = new BehaviorSubject(false)
|
|
||||||
private readonly polling$ = new BehaviorSubject(false)
|
|
||||||
private subs: Subscription[] = []
|
|
||||||
|
|
||||||
readonly connected$ = this.watchPatchConnection$().pipe(
|
|
||||||
filter(status => status === PatchConnection.Connected),
|
|
||||||
take(1),
|
|
||||||
shareReplay(),
|
|
||||||
)
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
// [wsSources, pollSources]
|
|
||||||
@Inject(PATCH_SOURCE) private readonly sources: Source<DataModel>[],
|
|
||||||
@Inject(BOOTSTRAPPER)
|
@Inject(BOOTSTRAPPER)
|
||||||
private readonly bootstrapper: Bootstrapper<DataModel>,
|
private readonly bootstrapper: Bootstrapper<DataModel>,
|
||||||
@Inject(PATCH_SOURCE$)
|
|
||||||
private readonly sources$: BehaviorSubject<Source<DataModel>[]>,
|
|
||||||
private readonly http: ApiService,
|
|
||||||
private readonly auth: AuthService,
|
|
||||||
private readonly storage: Storage,
|
|
||||||
private readonly patchDb: PatchDB<DataModel>,
|
private readonly patchDb: PatchDB<DataModel>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
init() {
|
start(): void {
|
||||||
this.sources$.next([this.sources[0], this.http])
|
// Early return if already started
|
||||||
this.patchConnection$.next(PatchConnection.Initializing)
|
if (this.sub) {
|
||||||
}
|
return
|
||||||
|
}
|
||||||
|
|
||||||
async start(): Promise<void> {
|
console.log('patchDB: STARTING')
|
||||||
this.init()
|
this.sub = this.patchDb.cache$
|
||||||
|
.pipe(
|
||||||
this.subs.push(
|
debounceTime(420),
|
||||||
// Connection Error
|
tap(cache => {
|
||||||
this.patchDb.connectionError$
|
this.bootstrapper.update(cache)
|
||||||
.pipe(
|
|
||||||
debounceTime(420),
|
|
||||||
withLatestFrom(this.polling$),
|
|
||||||
mergeMap(async ([e, polling]) => {
|
|
||||||
if (polling) {
|
|
||||||
console.log('patchDB: POLLING FAILED', e)
|
|
||||||
this.patchConnection$.next(PatchConnection.Disconnected)
|
|
||||||
await pauseFor(2000)
|
|
||||||
this.sources$.next([this.sources[1], this.http])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('patchDB: WEBSOCKET FAILED', e)
|
|
||||||
this.polling$.next(true)
|
|
||||||
this.sources$.next([this.sources[1], this.http])
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.subscribe({
|
|
||||||
complete: () => {
|
|
||||||
console.warn('patchDB: SYNC COMPLETE')
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
|
)
|
||||||
// RPC ERROR
|
.subscribe()
|
||||||
this.patchDb.rpcError$
|
|
||||||
.pipe(
|
|
||||||
tap(({ error }) => {
|
|
||||||
if (error.code === 34) {
|
|
||||||
console.log('patchDB: Unauthorized. Logging out.')
|
|
||||||
this.auth.setUnverified()
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.subscribe({
|
|
||||||
complete: () => {
|
|
||||||
console.warn('patchDB: SYNC COMPLETE')
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
|
|
||||||
// GOOD CONNECTION
|
|
||||||
this.patchDb.cache$
|
|
||||||
.pipe(
|
|
||||||
debounceTime(420),
|
|
||||||
withLatestFrom(this.patchConnection$, this.wsSuccess$, this.polling$),
|
|
||||||
tap(async ([cache, connection, wsSuccess, polling]) => {
|
|
||||||
this.bootstrapper.update(cache)
|
|
||||||
|
|
||||||
if (connection === PatchConnection.Initializing) {
|
|
||||||
console.log(
|
|
||||||
polling
|
|
||||||
? 'patchDB: POLL CONNECTED'
|
|
||||||
: 'patchDB: WEBSOCKET CONNECTED',
|
|
||||||
)
|
|
||||||
this.patchConnection$.next(PatchConnection.Connected)
|
|
||||||
if (!wsSuccess && !polling) {
|
|
||||||
console.log('patchDB: WEBSOCKET SUCCESS')
|
|
||||||
this.storage.set(this.WS_SUCCESS, 'true')
|
|
||||||
this.wsSuccess$.next(true)
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
connection === PatchConnection.Disconnected &&
|
|
||||||
wsSuccess
|
|
||||||
) {
|
|
||||||
console.log('patchDB: SWITCHING BACK TO WEBSOCKETS')
|
|
||||||
this.patchConnection$.next(PatchConnection.Initializing)
|
|
||||||
this.polling$.next(false)
|
|
||||||
this.sources$.next([this.sources[0], this.http])
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.subscribe({
|
|
||||||
complete: () => {
|
|
||||||
console.warn('patchDB: SYNC COMPLETE')
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stop(): void {
|
stop(): void {
|
||||||
console.log('patchDB: STOPPING')
|
// Early return if already stopped
|
||||||
this.patchConnection$.next(PatchConnection.Initializing)
|
if (!this.sub) {
|
||||||
this.patchDb.store.reset()
|
return
|
||||||
this.subs.forEach(x => x.unsubscribe())
|
}
|
||||||
this.subs = []
|
|
||||||
}
|
|
||||||
|
|
||||||
watchPatchConnection$(): Observable<PatchConnection> {
|
console.log('patchDB: STOPPING')
|
||||||
return this.patchConnection$.asObservable()
|
this.patchDb.store.reset()
|
||||||
|
this.sub.unsubscribe()
|
||||||
|
this.sub = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
@@ -168,10 +52,7 @@ export class PatchDbService {
|
|||||||
|
|
||||||
console.log('patchDB: WATCHING ', argsString)
|
console.log('patchDB: WATCHING ', argsString)
|
||||||
|
|
||||||
return this.patchConnection$.pipe(
|
return this.patchDb.store.watch$(...(args as [])).pipe(
|
||||||
filter(status => status === PatchConnection.Connected),
|
|
||||||
take(1),
|
|
||||||
switchMap(() => this.patchDb.store.watch$(...(args as []))),
|
|
||||||
tap(data => console.log('patchDB: NEW VALUE', argsString, data)),
|
tap(data => console.log('patchDB: NEW VALUE', argsString, data)),
|
||||||
catchError(e => {
|
catchError(e => {
|
||||||
console.error('patchDB: WATCH ERROR', e)
|
console.error('patchDB: WATCH ERROR', e)
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { Storage } from '@ionic/storage-angular'
|
import { Observable } from 'rxjs'
|
||||||
import { from, Observable, of } from 'rxjs'
|
import { map } from 'rxjs/operators'
|
||||||
import { mapTo, share, switchMap } from 'rxjs/operators'
|
|
||||||
|
|
||||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||||
import { AuthService } from 'src/app/services/auth.service'
|
import { AuthService } from 'src/app/services/auth.service'
|
||||||
|
|
||||||
@@ -12,23 +10,19 @@ import { AuthService } from 'src/app/services/auth.service'
|
|||||||
})
|
})
|
||||||
export class PatchMonitorService extends Observable<boolean> {
|
export class PatchMonitorService extends Observable<boolean> {
|
||||||
private readonly stream$ = this.authService.isVerified$.pipe(
|
private readonly stream$ = this.authService.isVerified$.pipe(
|
||||||
switchMap(verified => {
|
map(verified => {
|
||||||
if (verified) {
|
if (verified) {
|
||||||
return from(this.patch.start()).pipe(mapTo(true))
|
this.patch.start()
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
this.patch.stop()
|
this.patch.stop()
|
||||||
this.storage.clear()
|
return false
|
||||||
|
|
||||||
return of(false)
|
|
||||||
}),
|
}),
|
||||||
share(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly authService: AuthService,
|
private readonly authService: AuthService,
|
||||||
private readonly patch: PatchDbService,
|
private readonly patch: PatchDbService,
|
||||||
private readonly storage: Storage,
|
|
||||||
) {
|
) {
|
||||||
super(subscriber => this.stream$.subscribe(subscriber))
|
super(subscriber => this.stream$.subscribe(subscriber))
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
import { first } from 'rxjs/operators'
|
|
||||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||||
import { UIMarketplaceData } from 'src/app/services/patch-db/data-model'
|
import { UIMarketplaceData } from 'src/app/services/patch-db/data-model'
|
||||||
import { firstValueFrom } from 'rxjs'
|
import { filter, firstValueFrom } from 'rxjs'
|
||||||
|
|
||||||
export function getMarketplace(
|
export function getMarketplace(
|
||||||
patch: PatchDbService,
|
patch: PatchDbService,
|
||||||
): Promise<UIMarketplaceData> {
|
): Promise<UIMarketplaceData> {
|
||||||
return firstValueFrom(patch.watch$('ui', 'marketplace'))
|
return firstValueFrom(patch.watch$('ui', 'marketplace').pipe(filter(Boolean)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { first } from 'rxjs/operators'
|
|
||||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||||
import { firstValueFrom } from 'rxjs'
|
import { filter, firstValueFrom } from 'rxjs'
|
||||||
|
|
||||||
export function getPackage(
|
export function getPackage(
|
||||||
patch: PatchDbService,
|
patch: PatchDbService,
|
||||||
@@ -13,5 +12,5 @@ export function getPackage(
|
|||||||
export function getAllPackages(
|
export function getAllPackages(
|
||||||
patch: PatchDbService,
|
patch: PatchDbService,
|
||||||
): Promise<Record<string, PackageDataEntry>> {
|
): Promise<Record<string, PackageDataEntry>> {
|
||||||
return firstValueFrom(patch.watch$('package-data'))
|
return firstValueFrom(patch.watch$('package-data').pipe(filter(Boolean)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||||
import { ServerInfo } from 'src/app/services/patch-db/data-model'
|
import { ServerInfo } from 'src/app/services/patch-db/data-model'
|
||||||
import { firstValueFrom } from 'rxjs'
|
import { filter, firstValueFrom } from 'rxjs'
|
||||||
|
|
||||||
export function getServerInfo(patch: PatchDbService): Promise<ServerInfo> {
|
export function getServerInfo(patch: PatchDbService): Promise<ServerInfo> {
|
||||||
return firstValueFrom(patch.watch$('server-info'))
|
return firstValueFrom(patch.watch$('server-info').pipe(filter(Boolean)))
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user