From fc2be4241833fb6d3e798bbb455810079d69fa18 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Thu, 10 Apr 2025 13:51:05 -0600 Subject: [PATCH] sideload wip, websockets, styling, multiple todos (#2865) * sideload wip, websockets, styling, multiple todos * sideload * misc backend updates * chore: comments * prep for license and instructions display * comment for Matt * s9pk updates and 040 sdk * fix dependency error for actions * 0.4.0-beta.1 * beta.2 --------- Co-authored-by: Aiden McClelland Co-authored-by: waterplea Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> --- core/startos/src/auth.rs | 11 - core/startos/src/notifications.rs | 3 + core/startos/src/registry/package/index.rs | 6 +- core/startos/src/s9pk/git_hash.rs | 8 + core/startos/src/s9pk/v2/pack.rs | 26 +- core/startos/src/service/action.rs | 42 +++- sdk/base/lib/dependencies/dependencies.ts | 8 +- sdk/base/lib/osBindings/LoginParams.ts | 6 +- sdk/base/lib/osBindings/PackageVersionInfo.ts | 2 +- sdk/base/lib/osBindings/Session.ts | 6 +- sdk/base/lib/osBindings/Sessions.ts | 6 +- sdk/base/lib/s9pk/index.ts | 69 +++++- sdk/package/lib/StartSdk.ts | 28 ++- sdk/package/lib/inits/setupInit.ts | 9 +- sdk/package/lib/inits/setupInstall.ts | 53 ++-- sdk/package/package-lock.json | 4 +- sdk/package/package.json | 2 +- web/package-lock.json | 234 +++++++++--------- web/package.json | 24 +- .../src/components/menu/menu.component.scss | 2 +- .../src/pages/list/item/item.component.html | 18 -- .../src/pages/show/about/about.component.ts | 4 +- .../additional/additional-item.component.ts | 4 +- .../show/additional/additional.component.html | 97 +------- .../show/additional/additional.component.ts | 8 +- .../dependencies/dependencies.component.ts | 4 +- .../dependencies/dependency-item.component.ts | 6 +- web/projects/marketplace/src/types.ts | 19 +- .../src/app/services/mock-api.service.ts | 2 +- .../initializing/initializing.component.ts | 7 +- .../src/directives/safe-links.directive.ts | 2 +- .../shared/src/services/error.service.ts | 2 +- .../shared/src/services/setup-logs.service.ts | 13 +- .../shared/src/util/format-progress.ts | 25 +- web/projects/shared/src/util/misc.util.ts | 3 + web/projects/shared/styles/shared.scss | 2 +- web/projects/shared/styles/taiga.scss | 2 +- .../app/components/refresh-alert.component.ts | 1 + .../routes/initializing/initializing.page.ts | 26 +- .../ui/src/app/routes/login/login.page.ts | 1 - .../routes/portal/components/form/control.ts | 2 +- .../components/header/header.component.ts | 2 +- .../interfaces/actions.component.ts | 1 + .../interfaces/interface.component.ts | 1 + .../components/interfaces/status.component.ts | 23 ++ .../portal/components/tabs.component.ts | 2 +- .../src/app/routes/portal/portal.component.ts | 2 +- .../backups/components/upcoming.component.ts | 2 +- .../components/controls.component.ts | 4 +- .../marketplace/modals/preview.component.ts | 11 +- .../marketplace/services/alerts.service.ts | 4 +- .../portal/routes/metrics/metrics.service.ts | 14 +- .../routes/metrics/temperature.component.ts | 2 +- .../portal/routes/metrics/time.component.ts | 22 +- .../portal/routes/metrics/uptime.component.ts | 1 + .../services/components/action.component.ts | 5 +- ...ons.component.ts => controls.component.ts} | 40 +-- .../components/interface.component.ts | 7 +- .../services/components/status.component.ts | 11 +- .../services/dashboard/dashboard.component.ts | 9 +- .../services/dashboard/status.component.ts | 15 +- .../services/pipes/install-progress.pipe.ts | 25 +- .../routes/services/routes/about.component.ts | 2 +- .../services/routes/actions.component.ts | 28 ++- .../services/routes/interface.component.ts | 16 +- .../services/routes/service.component.ts | 6 +- .../routes/services/types/dependency-info.ts | 9 - .../routes/services/types/mapped-interface.ts | 16 -- .../routes/sideload/package.component.ts | 175 ++++++------- .../routes/sideload/sideload.component.ts | 28 +-- .../routes/sideload/sideload.service.ts | 56 ----- .../portal/routes/sideload/sideload.utils.ts | 117 ++------- .../system/routes/acme/acme.component.ts | 2 +- .../routes/domains/domains.component.ts | 6 +- .../system/routes/email/email.component.ts | 2 +- .../routes/interfaces/interfaces.component.ts | 8 +- .../routes/proxies/proxies.component.ts | 2 +- .../routes/sessions/platform-info.pipe.ts | 51 ++-- .../routes/sessions/sessions.component.ts | 1 - .../system/routes/sessions/table.component.ts | 6 +- .../system/routes/wifi/wifi.component.ts | 2 +- .../ui/src/app/services/api/api.fixures.ts | 37 +-- .../ui/src/app/services/api/api.types.ts | 25 +- .../ui/src/app/services/api/mock-patch.ts | 2 +- .../ui/src/app/services/form.service.ts | 131 ++-------- .../src/app/services/marketplace.service.ts | 7 +- web/projects/ui/src/styles.scss | 2 +- web/tsconfig.json | 1 - 88 files changed, 773 insertions(+), 965 deletions(-) create mode 100644 web/projects/ui/src/app/routes/portal/components/interfaces/status.component.ts rename web/projects/ui/src/app/routes/portal/routes/services/components/{actions.component.ts => controls.component.ts} (69%) delete mode 100644 web/projects/ui/src/app/routes/portal/routes/services/types/dependency-info.ts delete mode 100644 web/projects/ui/src/app/routes/portal/routes/services/types/mapped-interface.ts delete mode 100644 web/projects/ui/src/app/routes/portal/routes/sideload/sideload.service.ts diff --git a/core/startos/src/auth.rs b/core/startos/src/auth.rs index 2bc85b7db..58aa54cc9 100644 --- a/core/startos/src/auth.rs +++ b/core/startos/src/auth.rs @@ -197,9 +197,6 @@ pub struct LoginParams { user_agent: Option, #[serde(default)] ephemeral: bool, - #[serde(default)] - #[ts(type = "any")] - metadata: Value, } #[instrument(skip_all)] @@ -209,7 +206,6 @@ pub async fn login_impl( password, user_agent, ephemeral, - metadata, }: LoginParams, ) -> Result { let password = password.unwrap_or_default().decrypt(&ctx)?; @@ -224,7 +220,6 @@ pub async fn login_impl( logged_in: Utc::now(), last_active: Utc::now(), user_agent, - metadata, }, ) }); @@ -240,7 +235,6 @@ pub async fn login_impl( logged_in: Utc::now(), last_active: Utc::now(), user_agent, - metadata, }, )?; @@ -277,10 +271,7 @@ pub struct Session { pub logged_in: DateTime, #[ts(type = "string")] pub last_active: DateTime, - #[ts(skip)] pub user_agent: Option, - #[ts(type = "any")] - pub metadata: Value, } #[derive(Deserialize, Serialize, TS)] @@ -327,7 +318,6 @@ fn display_sessions(params: WithIoFormat, arg: SessionList) { "LOGGED IN", "LAST ACTIVE", "USER AGENT", - "METADATA", ]); for (id, session) in arg.sessions.0 { let mut row = row![ @@ -335,7 +325,6 @@ fn display_sessions(params: WithIoFormat, arg: SessionList) { &format!("{}", session.logged_in), &format!("{}", session.last_active), session.user_agent.as_deref().unwrap_or("N/A"), - &format!("{}", session.metadata), ]; if Some(id) == arg.current { row.iter_mut() diff --git a/core/startos/src/notifications.rs b/core/startos/src/notifications.rs index e3ddfe848..d982f3962 100644 --- a/core/startos/src/notifications.rs +++ b/core/startos/src/notifications.rs @@ -186,6 +186,7 @@ pub async fn remove_before( Ok(()) }) .await + .result } pub async fn mark_seen( @@ -213,6 +214,7 @@ pub async fn mark_seen( Ok(()) }) .await + .result } pub async fn mark_seen_before( @@ -240,6 +242,7 @@ pub async fn mark_seen_before( Ok(()) }) .await + .result } pub async fn mark_unseen( diff --git a/core/startos/src/registry/package/index.rs b/core/startos/src/registry/package/index.rs index 9973bae7e..d48963267 100644 --- a/core/startos/src/registry/package/index.rs +++ b/core/startos/src/registry/package/index.rs @@ -72,7 +72,7 @@ pub struct PackageVersionInfo { pub icon: DataUrl<'static>, pub description: Description, pub release_notes: String, - pub git_hash: GitHash, + pub git_hash: Option, #[ts(type = "string")] pub license: InternedString, #[ts(type = "string")] @@ -115,7 +115,7 @@ impl PackageVersionInfo { icon: s9pk.icon_data_url().await?, description: manifest.description.clone(), release_notes: manifest.release_notes.clone(), - git_hash: manifest.git_hash.clone().or_not_found("git hash")?, + git_hash: manifest.git_hash.clone(), license: manifest.license.clone(), wrapper_repo: manifest.wrapper_repo.clone(), upstream_repo: manifest.upstream_repo.clone(), @@ -153,7 +153,7 @@ impl PackageVersionInfo { br -> "DESCRIPTION", &textwrap::wrap(&self.description.long, 80).join("\n") ]); - table.add_row(row![br -> "GIT HASH", AsRef::::as_ref(&self.git_hash)]); + table.add_row(row![br -> "GIT HASH", self.git_hash.as_deref().unwrap_or("N/A")]); table.add_row(row![br -> "LICENSE", &self.license]); table.add_row(row![br -> "PACKAGE REPO", &self.wrapper_repo.to_string()]); table.add_row(row![br -> "SERVICE REPO", &self.upstream_repo.to_string()]); diff --git a/core/startos/src/s9pk/git_hash.rs b/core/startos/src/s9pk/git_hash.rs index 762ef8704..a4c9bb2b9 100644 --- a/core/startos/src/s9pk/git_hash.rs +++ b/core/startos/src/s9pk/git_hash.rs @@ -1,3 +1,4 @@ +use std::ops::Deref; use std::path::Path; use tokio::process::Command; @@ -66,6 +67,13 @@ impl AsRef for GitHash { } } +impl Deref for GitHash { + type Target = str; + fn deref(&self) -> &Self::Target { + self.as_ref() + } +} + // #[tokio::test] // async fn test_githash_for_current() { // let answer: GitHash = GitHash::from_path(std::env::current_dir().unwrap()) diff --git a/core/startos/src/s9pk/v2/pack.rs b/core/startos/src/s9pk/v2/pack.rs index ce8d9e3a6..4e1f271ba 100644 --- a/core/startos/src/s9pk/v2/pack.rs +++ b/core/startos/src/s9pk/v2/pack.rs @@ -26,7 +26,6 @@ use crate::s9pk::merkle_archive::source::{ into_dyn_read, ArchiveSource, DynFileSource, DynRead, FileSource, TmpSource, }; use crate::s9pk::merkle_archive::{Entry, MerkleArchive}; -use crate::s9pk::v2::recipe::DirRecipe; use crate::s9pk::v2::SIG_CONTEXT; use crate::s9pk::S9pk; use crate::util::io::{create_file, open_file, TmpDir}; @@ -736,25 +735,34 @@ pub async fn pack(ctx: CliContext, params: PackParams) -> Result<(), Error> { let dep_path = Path::new("dependencies").join(id); to_insert.push(( dep_path.join("metadata.json"), - Entry::file(PackSource::Buffered( - IoFormat::Json - .to_vec(&DependencyMetadata { - title: s9pk.as_manifest().title.clone(), - })? - .into(), + Entry::file(TmpSource::new( + tmp_dir.clone(), + PackSource::Buffered( + IoFormat::Json + .to_vec(&DependencyMetadata { + title: s9pk.as_manifest().title.clone(), + })? + .into(), + ), )), )); let icon = s9pk.icon().await?; to_insert.push(( dep_path.join(&*icon.0), - Entry::file(PackSource::Buffered( - icon.1.expect_file()?.to_vec(icon.1.hash()).await?.into(), + Entry::file(TmpSource::new( + tmp_dir.clone(), + PackSource::Buffered(icon.1.expect_file()?.to_vec(icon.1.hash()).await?.into()), )), )); } else { warn!("no s9pk specified for {id}, leaving metadata empty"); } } + for (path, source) in to_insert { + s9pk.as_archive_mut() + .contents_mut() + .insert_path(path, source)?; + } s9pk.validate_and_filter(None)?; diff --git a/core/startos/src/service/action.rs b/core/startos/src/service/action.rs index f5273cf0c..bb1af6432 100644 --- a/core/startos/src/service/action.rs +++ b/core/startos/src/service/action.rs @@ -5,7 +5,10 @@ use imbl_value::json; use models::{ActionId, PackageId, ProcedureName, ReplayId}; use crate::action::{ActionInput, ActionResult}; -use crate::db::model::package::{ActionRequestCondition, ActionRequestEntry, ActionRequestInput}; +use crate::db::model::package::{ + ActionRequestCondition, ActionRequestEntry, ActionRequestInput, ActionVisibility, + AllowedStatuses, +}; use crate::prelude::*; use crate::rpc_continuations::Guid; use crate::service::{Service, ServiceActor}; @@ -123,12 +126,44 @@ impl Handler for ServiceActor { &mut self, id: Guid, RunAction { - id: action_id, + id: ref action_id, input, }: RunAction, _: &BackgroundJobQueue, ) -> Self::Response { let container = &self.0.persistent_container; + let package_id = &self.0.id; + let action = self + .0 + .ctx + .db + .peek() + .await + .into_public() + .into_package_data() + .into_idx(package_id) + .or_not_found(package_id)? + .into_actions() + .into_idx(&action_id) + .or_not_found(lazy_format!("{package_id} action {action_id}"))? + .de()?; + if !matches!(&action.visibility, ActionVisibility::Enabled) { + return Err(Error::new( + eyre!("action {action_id} is disabled"), + ErrorKind::Action, + )); + } + let running = container.state.borrow().running_status.as_ref().is_some(); + if match action.allowed_statuses { + AllowedStatuses::OnlyRunning => !running, + AllowedStatuses::OnlyStopped => running, + _ => false, + } { + return Err(Error::new( + eyre!("service is not in allowed status for {action_id}"), + ErrorKind::Action, + )); + } let result = container .execute::>( id, @@ -140,7 +175,6 @@ impl Handler for ServiceActor { ) .await .with_kind(ErrorKind::Action)?; - let package_id = &self.0.id; self.0 .ctx .db @@ -150,7 +184,7 @@ impl Handler for ServiceActor { Ok(update_requested_actions( requested_actions, package_id, - &action_id, + action_id, &input, true, )) diff --git a/sdk/base/lib/dependencies/dependencies.ts b/sdk/base/lib/dependencies/dependencies.ts index 20049b5e8..d2d50eb51 100644 --- a/sdk/base/lib/dependencies/dependencies.ts +++ b/sdk/base/lib/dependencies/dependencies.ts @@ -66,7 +66,9 @@ export async function checkDependencies< return dep.requirement.kind !== "running" || dep.result.isRunning } const actionsSatisfied = (packageId: DependencyId) => - Object.keys(find(packageId).result.requestedActions).length === 0 + Object.entries(find(packageId).result.requestedActions).filter( + ([_, req]) => req.active && req.request.severity === "critical", + ).length === 0 const healthCheckSatisfied = ( packageId: DependencyId, healthCheckId?: HealthCheckId, @@ -129,7 +131,9 @@ export async function checkDependencies< } const throwIfActionsNotSatisfied = (packageId: DependencyId) => { const dep = find(packageId) - const reqs = Object.keys(dep.result.requestedActions) + const reqs = Object.entries(dep.result.requestedActions) + .filter(([_, req]) => req.active && req.request.severity === "critical") + .map(([id, _]) => id) if (reqs.length) { throw new Error( `The following action requests have not been fulfilled: ${reqs.join(", ")}`, diff --git a/sdk/base/lib/osBindings/LoginParams.ts b/sdk/base/lib/osBindings/LoginParams.ts index acaf5b8a1..a569e6c8e 100644 --- a/sdk/base/lib/osBindings/LoginParams.ts +++ b/sdk/base/lib/osBindings/LoginParams.ts @@ -1,8 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { PasswordType } from "./PasswordType" -export type LoginParams = { - password: PasswordType | null - ephemeral: boolean - metadata: any -} +export type LoginParams = { password: PasswordType | null; ephemeral: boolean } diff --git a/sdk/base/lib/osBindings/PackageVersionInfo.ts b/sdk/base/lib/osBindings/PackageVersionInfo.ts index c71fd5921..a52b5feaa 100644 --- a/sdk/base/lib/osBindings/PackageVersionInfo.ts +++ b/sdk/base/lib/osBindings/PackageVersionInfo.ts @@ -14,7 +14,7 @@ export type PackageVersionInfo = { icon: DataUrl description: Description releaseNotes: string - gitHash: GitHash + gitHash: GitHash | null license: string wrapperRepo: string upstreamRepo: string diff --git a/sdk/base/lib/osBindings/Session.ts b/sdk/base/lib/osBindings/Session.ts index f9cffaf36..36ebd2766 100644 --- a/sdk/base/lib/osBindings/Session.ts +++ b/sdk/base/lib/osBindings/Session.ts @@ -1,3 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Session = { loggedIn: string; lastActive: string; metadata: any } +export type Session = { + loggedIn: string + lastActive: string + userAgent: string | null +} diff --git a/sdk/base/lib/osBindings/Sessions.ts b/sdk/base/lib/osBindings/Sessions.ts index bb153ddb4..0f43f1d01 100644 --- a/sdk/base/lib/osBindings/Sessions.ts +++ b/sdk/base/lib/osBindings/Sessions.ts @@ -1,5 +1,9 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. export type Sessions = { - [key: string]: { loggedIn: string; lastActive: string; metadata: any } + [key: string]: { + loggedIn: string + lastActive: string + userAgent: string | null + } } diff --git a/sdk/base/lib/s9pk/index.ts b/sdk/base/lib/s9pk/index.ts index 0eadb6ab6..9c16455c2 100644 --- a/sdk/base/lib/s9pk/index.ts +++ b/sdk/base/lib/s9pk/index.ts @@ -1,6 +1,14 @@ -import { DataUrl, Manifest, MerkleArchiveCommitment } from "../osBindings" +import { + DataUrl, + DependencyMetadata, + Manifest, + MerkleArchiveCommitment, + PackageId, +} from "../osBindings" import { ArrayBufferReader, MerkleArchive } from "./merkleArchive" import mime from "mime-types" +import { DirectoryContents } from "./merkleArchive/directoryContents" +import { FileContents } from "./merkleArchive/fileContents" const magicAndVersion = new Uint8Array([59, 59, 2]) @@ -65,4 +73,63 @@ export class S9pk { ).toString("base64") ) } + + async dependencyMetadataFor(id: PackageId) { + const entry = this.archive.contents.getPath([ + "dependencies", + id, + "metadata.json", + ]) + if (!entry) return null + return JSON.parse( + new TextDecoder().decode(await entry.verifiedFileContents()), + ) as { title: string } + } + + async dependencyIconFor(id: PackageId) { + const dir = this.archive.contents.getPath(["dependencies", id]) + if (!dir || !(dir.contents instanceof DirectoryContents)) return null + const iconName = Object.keys(dir.contents.contents).find( + (name) => + name.startsWith("icon.") && + (mime.contentType(name) || null)?.startsWith("image/"), + ) + if (!iconName) return null + return ( + `data:${mime.contentType(iconName)};base64,` + + Buffer.from( + await dir.contents.getPath([iconName])!.verifiedFileContents(), + ).toString("base64") + ) + } + + async dependencyMetadata(): Promise> { + return Object.fromEntries( + await Promise.all( + Object.entries(this.manifest.dependencies).map(async ([id, info]) => [ + id, + { + ...(await this.dependencyMetadataFor(id)), + icon: await this.dependencyIconFor(id), + description: info.description, + optional: info.optional, + }, + ]), + ), + ) + } + + async instructions(): Promise { + const file = this.archive.contents.getPath(["instructions.md"]) + if (!file || !(file.contents instanceof FileContents)) + throw new Error("instructions.md not found in archive") + return new TextDecoder().decode(await file.verifiedFileContents()) + } + + async license(): Promise { + const file = this.archive.contents.getPath(["LICENSE.md"]) + if (!file || !(file.contents instanceof FileContents)) + throw new Error("instructions.md not found in archive") + return new TextDecoder().decode(await file.verifiedFileContents()) + } } diff --git a/sdk/package/lib/StartSdk.ts b/sdk/package/lib/StartSdk.ts index 536d254ad..bdabd0ae6 100644 --- a/sdk/package/lib/StartSdk.ts +++ b/sdk/package/lib/StartSdk.ts @@ -30,7 +30,12 @@ import { HealthCheck } from "./health/HealthCheck" import { checkPortListening } from "./health/checkFns/checkPortListening" import { checkWebUrl, runHealthScript } from "./health/checkFns" import { List } from "../../base/lib/actions/input/builder/list" -import { Install, InstallFn } from "./inits/setupInstall" +import { + Install, + InstallFn, + PostInstall, + PreInstall, +} from "./inits/setupInstall" import { SetupBackupsParams, setupBackups } from "./backup/setupBackups" import { UninstallFn, setupUninstall } from "./inits/setupUninstall" import { setupMain } from "./mainFn" @@ -571,12 +576,24 @@ export class StartSdk { setupDependencies: setupDependencies, setupInit: setupInit, /** - * @description Use this function to execute arbitrary logic *once*, on initial install only. + * @description Use this function to execute arbitrary logic *once*, on initial install *before* interfaces, actions, and dependencies are updated. + * @example + * In the this example, we initialize a config file + * + * ``` + const preInstall = sdk.setupPreInstall(async ({ effects }) => { + await configFile.write(effects, { name: 'World' }) + }) + * ``` + */ + setupPreInstall: (fn: InstallFn) => PreInstall.of(fn), + /** + * @description Use this function to execute arbitrary logic *once*, on initial install *after* interfaces, actions, and dependencies are updated. * @example * In the this example, we bootstrap our Store with a random, 16-char admin password. * * ``` - const install = sdk.setupInstall(async ({ effects }) => { + const postInstall = sdk.setupPostInstall(async ({ effects }) => { await sdk.store.setOwn( effects, sdk.StorePath.adminPassword, @@ -588,10 +605,7 @@ export class StartSdk { }) * ``` */ - setupInstall: ( - fn: InstallFn, - preFn?: InstallFn, - ) => Install.of(fn, preFn), + setupPostInstall: (fn: InstallFn) => PostInstall.of(fn), /** * @description Use this function to determine how this service will be hosted and served. The function executes on service install, service update, and inputSpec save. * diff --git a/sdk/package/lib/inits/setupInit.ts b/sdk/package/lib/inits/setupInit.ts index 21cfd1641..476dd8cd4 100644 --- a/sdk/package/lib/inits/setupInit.ts +++ b/sdk/package/lib/inits/setupInit.ts @@ -5,12 +5,13 @@ import { ExposedStorePaths } from "../../../base/lib/types" import * as T from "../../../base/lib/types" import { StorePath } from "../util" import { VersionGraph } from "../version/VersionGraph" -import { Install } from "./setupInstall" +import { PostInstall, PreInstall } from "./setupInstall" import { Uninstall } from "./setupUninstall" export function setupInit( versions: VersionGraph, - install: Install, + preInstall: PreInstall, + postInstall: PostInstall, uninstall: Uninstall, setServiceInterfaces: UpdateServiceInterfaces, setDependencies: (options: { @@ -34,7 +35,7 @@ export function setupInit( to: versions.currentVersion(), }) } else { - await install.install(opts) + await postInstall.postInstall(opts) await opts.effects.setDataVersion({ version: versions.current.options.version, }) @@ -61,7 +62,7 @@ export function setupInit( path: "" as StorePath, value: initStore, }) - await install.preInstall(opts) + await preInstall.preInstall(opts) } await setServiceInterfaces({ ...opts, diff --git a/sdk/package/lib/inits/setupInstall.ts b/sdk/package/lib/inits/setupInstall.ts index 708367e20..884c61fec 100644 --- a/sdk/package/lib/inits/setupInstall.ts +++ b/sdk/package/lib/inits/setupInstall.ts @@ -4,34 +4,57 @@ export type InstallFn = (opts: { effects: T.Effects }) => Promise export class Install { - private constructor( - readonly fn: InstallFn, - readonly preFn?: InstallFn, - ) {} + protected constructor(readonly fn: InstallFn) {} +} + +export class PreInstall extends Install< + Manifest, + Store +> { + private constructor(fn: InstallFn) { + super(fn) + } static of( fn: InstallFn, - preFn?: InstallFn, ) { - return new Install(fn, preFn) + return new PreInstall(fn) } - async install({ effects }: Parameters[0]) { + async preInstall({ effects }: Parameters[0]) { await this.fn({ effects, }) } +} - async preInstall({ effects }: Parameters[0]) { - this.preFn && - (await this.preFn({ - effects, - })) +export function setupPreInstall( + fn: InstallFn, +) { + return PreInstall.of(fn) +} + +export class PostInstall extends Install< + Manifest, + Store +> { + private constructor(fn: InstallFn) { + super(fn) + } + static of( + fn: InstallFn, + ) { + return new PostInstall(fn) + } + + async postInstall({ effects }: Parameters[0]) { + await this.fn({ + effects, + }) } } -export function setupInstall( +export function setupPostInstall( fn: InstallFn, - preFn?: InstallFn, ) { - return Install.of(fn, preFn) + return PostInstall.of(fn) } diff --git a/sdk/package/package-lock.json b/sdk/package/package-lock.json index 9eb3add44..e00df38ba 100644 --- a/sdk/package/package-lock.json +++ b/sdk/package/package-lock.json @@ -1,12 +1,12 @@ { "name": "@start9labs/start-sdk", - "version": "0.3.6-beta.20", + "version": "0.4.0-beta.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@start9labs/start-sdk", - "version": "0.3.6-beta.20", + "version": "0.4.0-beta.2", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/sdk/package/package.json b/sdk/package/package.json index 31dcf9799..677a0b74c 100644 --- a/sdk/package/package.json +++ b/sdk/package/package.json @@ -1,6 +1,6 @@ { "name": "@start9labs/start-sdk", - "version": "0.3.6-beta.20", + "version": "0.4.0-beta.2", "description": "Software development kit to facilitate packaging services for StartOS", "main": "./package/lib/index.js", "types": "./package/lib/index.d.ts", diff --git a/web/package-lock.json b/web/package-lock.json index 690e44048..404f4f0fb 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -25,18 +25,18 @@ "@noble/hashes": "^1.4.0", "@start9labs/argon2": "^0.2.2", "@start9labs/start-sdk": "file:../sdk/baseDist", - "@taiga-ui/addon-charts": "4.30.0", - "@taiga-ui/addon-commerce": "4.30.0", - "@taiga-ui/addon-mobile": "4.30.0", - "@taiga-ui/addon-table": "4.30.0", - "@taiga-ui/cdk": "4.30.0", - "@taiga-ui/core": "4.30.0", - "@taiga-ui/event-plugins": "4.5.0", - "@taiga-ui/experimental": "4.30.0", - "@taiga-ui/icons": "4.30.0", - "@taiga-ui/kit": "4.30.0", - "@taiga-ui/layout": "4.30.0", - "@taiga-ui/legacy": "4.30.0", + "@taiga-ui/addon-charts": "4.32.0", + "@taiga-ui/addon-commerce": "4.32.0", + "@taiga-ui/addon-mobile": "4.32.0", + "@taiga-ui/addon-table": "4.32.0", + "@taiga-ui/cdk": "4.32.0", + "@taiga-ui/core": "4.32.0", + "@taiga-ui/event-plugins": "4.5.1", + "@taiga-ui/experimental": "4.32.0", + "@taiga-ui/icons": "4.32.0", + "@taiga-ui/kit": "4.32.0", + "@taiga-ui/layout": "4.32.0", + "@taiga-ui/legacy": "4.32.0", "@taiga-ui/polymorpheus": "4.9.0", "@tinkoff/ng-dompurify": "4.0.0", "ansi-to-html": "^0.7.2", @@ -3480,9 +3480,9 @@ } }, "node_modules/@ng-web-apis/common": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/@ng-web-apis/common/-/common-4.11.1.tgz", - "integrity": "sha512-fXbcMrd/+L+9j9knbgXbDwYe30H4Wt0hQzvqyhpXTVrc0jYwlk3MJTYrnazKz5HvP9318caEv5n4qt3HMf5uPQ==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@ng-web-apis/common/-/common-4.12.0.tgz", + "integrity": "sha512-OG4ChsEWQ0IbGJ+WrJAiOY5X4jF8f5YUCss961taPeiyhvwtUo4zAuX3UvtV/iJSt8XZ41jaOYFTyMIBGubv4Q==", "license": "Apache-2.0", "peer": true, "dependencies": { @@ -3495,9 +3495,9 @@ } }, "node_modules/@ng-web-apis/intersection-observer": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/@ng-web-apis/intersection-observer/-/intersection-observer-4.11.1.tgz", - "integrity": "sha512-KjODVVx20yG/U5bnPvp5voihL5DSVFuYwZVY9DNRvaFIcQPMy1tL1t9/oJOdxj7zUSFDL8+Z0RoJbsvArezuSg==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@ng-web-apis/intersection-observer/-/intersection-observer-4.12.0.tgz", + "integrity": "sha512-7MW0y6BrjLKCTUGb5YfsEPYpn17wPmGj8J+2A980ntGNveo9+DILz3KpFHkpS6G6bJGtnD36exU2YvfTUKiyXA==", "license": "Apache-2.0", "peer": true, "dependencies": { @@ -3505,13 +3505,13 @@ }, "peerDependencies": { "@angular/core": ">=16.0.0", - "@ng-web-apis/common": ">=4.11.1" + "@ng-web-apis/common": ">=4.12.0" } }, "node_modules/@ng-web-apis/mutation-observer": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/@ng-web-apis/mutation-observer/-/mutation-observer-4.11.1.tgz", - "integrity": "sha512-YFnkGFE0gd03q4HBJL+WPl3YZRZNq7dVV8yD5uqr0ZCDgmOAMBilrp42FuHBPaYkF73Rs2EpKsKHWz1jASqBbQ==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@ng-web-apis/mutation-observer/-/mutation-observer-4.12.0.tgz", + "integrity": "sha512-Yyz0jpQoGgWzQEhjRH8xC9Umxr2W0hYktuYDNxrmnr6GnBejcfkE+wCon7FhCt6h7BrqR3+Z4cg7TvnyUVHU6Q==", "license": "Apache-2.0", "peer": true, "dependencies": { @@ -3519,13 +3519,13 @@ }, "peerDependencies": { "@angular/core": ">=16.0.0", - "@ng-web-apis/common": ">=4.11.1" + "@ng-web-apis/common": ">=4.12.0" } }, "node_modules/@ng-web-apis/platform": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/@ng-web-apis/platform/-/platform-4.11.1.tgz", - "integrity": "sha512-BrhkUIEEAD7wcwR65LSXHYOD6L3IvAb4aV94S8tzxUNeGUPwikX5glQJBT1UwkHWXQjANPKTCNyK1LO+cMPgkw==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@ng-web-apis/platform/-/platform-4.12.0.tgz", + "integrity": "sha512-OuwV9OERPvQD+QxS2q84pWg60GlL9O66zl0VBLDR8ARMslBfCg1m70LlbLfQI4mlt4QZzTliUVps1JaGyAEKYA==", "license": "Apache-2.0", "peer": true, "dependencies": { @@ -3533,9 +3533,9 @@ } }, "node_modules/@ng-web-apis/resize-observer": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/@ng-web-apis/resize-observer/-/resize-observer-4.11.1.tgz", - "integrity": "sha512-q8eJ6sovnMhfqIULN1yyhqT35Y2a60vB42p9CUBWPeeVammU+QHE/imPCMCJgnti9cdPZfnyI/+TeYNIUg7mzg==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@ng-web-apis/resize-observer/-/resize-observer-4.12.0.tgz", + "integrity": "sha512-ekLcZnqap9OBcoTtOD0/tcOy/STFrxSu3WR0yU9yEM0n7S1mOsKOnXIY4PMAxxWV4LMs91P00wzFHfNNnuOS/g==", "license": "Apache-2.0", "peer": true, "dependencies": { @@ -3543,13 +3543,13 @@ }, "peerDependencies": { "@angular/core": ">=16.0.0", - "@ng-web-apis/common": ">=4.11.1" + "@ng-web-apis/common": ">=4.12.0" } }, "node_modules/@ng-web-apis/screen-orientation": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/@ng-web-apis/screen-orientation/-/screen-orientation-4.11.1.tgz", - "integrity": "sha512-HS/kWTgVjXVDqMLcJbl5uty+1sV10m9PeDag74tzktIDAB06diFQJQGfcQaA0o0IBisT3fOysf9gHV5sXxSOFw==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@ng-web-apis/screen-orientation/-/screen-orientation-4.12.0.tgz", + "integrity": "sha512-oqZqc9TTpEBNNinVSFjEl/plhoRxh07zMtdG6VMnuY6Lng2l1jfC7vKru2rjEskR+0sYgN8Y8Ttov03i8I9GHg==", "license": "Apache-2.0", "peer": true, "dependencies": { @@ -3557,7 +3557,7 @@ }, "peerDependencies": { "@angular/core": ">=16.0.0", - "@ng-web-apis/common": ">=4.11.1", + "@ng-web-apis/common": ">=4.12.0", "rxjs": ">=7.0.0" } }, @@ -4418,9 +4418,9 @@ "link": true }, "node_modules/@taiga-ui/addon-charts": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/addon-charts/-/addon-charts-4.30.0.tgz", - "integrity": "sha512-QrM2Oh4hUcg/I0K3KWFkc/dbTCYZn2n5GU2FSpZaK6I7pwjfRoMjBU7vswPLVVdmgeWTJxxoQlbfYnbUbkMAJw==", + "version": "4.32.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/addon-charts/-/addon-charts-4.32.0.tgz", + "integrity": "sha512-VhGkBxwfra5eijSvZdXhMKOWEnFMESo5TX3OfsahIXWJXivwguvIc63rIhHYq2uC+t5sj1kINveO4yLqOeAm/Q==", "license": "Apache-2.0", "dependencies": { "tslib": ">=2.8.1" @@ -4428,16 +4428,16 @@ "peerDependencies": { "@angular/common": ">=16.0.0", "@angular/core": ">=16.0.0", - "@ng-web-apis/common": "^4.11.1", - "@taiga-ui/cdk": "^4.30.0", - "@taiga-ui/core": "^4.30.0", + "@ng-web-apis/common": "^4.12.0", + "@taiga-ui/cdk": "^4.32.0", + "@taiga-ui/core": "^4.32.0", "@taiga-ui/polymorpheus": "^4.9.0" } }, "node_modules/@taiga-ui/addon-commerce": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/addon-commerce/-/addon-commerce-4.30.0.tgz", - "integrity": "sha512-6diktxvxMpWjbEHXThS0pTrURdUiF/47jf2jdBFkMwX3BbbekisM1qkwxY24V7q8fN0IIxfO8CVEjTeLRrCw5g==", + "version": "4.32.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/addon-commerce/-/addon-commerce-4.32.0.tgz", + "integrity": "sha512-AC3VU/RVTNapS8ltSAemZPeDb2CopJEj298rI3Vl4qER1oVl0zunmWVy5ncwK1F1zWKU2/QNDjjo8yKYWeU/Nw==", "license": "Apache-2.0", "dependencies": { "tslib": ">=2.8.1" @@ -4449,19 +4449,19 @@ "@maskito/angular": "^3.5.0", "@maskito/core": "^3.5.0", "@maskito/kit": "^3.5.0", - "@ng-web-apis/common": "^4.11.1", - "@taiga-ui/cdk": "^4.30.0", - "@taiga-ui/core": "^4.30.0", - "@taiga-ui/i18n": "^4.30.0", - "@taiga-ui/kit": "^4.30.0", + "@ng-web-apis/common": "^4.12.0", + "@taiga-ui/cdk": "^4.32.0", + "@taiga-ui/core": "^4.32.0", + "@taiga-ui/i18n": "^4.32.0", + "@taiga-ui/kit": "^4.32.0", "@taiga-ui/polymorpheus": "^4.9.0", "rxjs": ">=7.0.0" } }, "node_modules/@taiga-ui/addon-mobile": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/addon-mobile/-/addon-mobile-4.30.0.tgz", - "integrity": "sha512-8cYyU0UDLUd74v+Zjs4m9S4AsSWchUojAexDLvaAHzfi0x+tdtA+ZN0h49v8AmOWHK0v69z4FMjyyc52p/jiDw==", + "version": "4.32.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/addon-mobile/-/addon-mobile-4.32.0.tgz", + "integrity": "sha512-pUoHWyILPj6KIAhna1JDzz48c2nCjqYb1tb7AL3LQ3qfNwAbg9fvjBIfrgWMhW0LaDeh5+FfrS7oiO/ERcHTLg==", "license": "Apache-2.0", "dependencies": { "tslib": ">=2.8.1" @@ -4470,19 +4470,19 @@ "@angular/cdk": ">=16.0.0", "@angular/common": ">=16.0.0", "@angular/core": ">=16.0.0", - "@ng-web-apis/common": "^4.11.1", - "@taiga-ui/cdk": "^4.30.0", - "@taiga-ui/core": "^4.30.0", - "@taiga-ui/kit": "^4.30.0", - "@taiga-ui/layout": "^4.30.0", + "@ng-web-apis/common": "^4.12.0", + "@taiga-ui/cdk": "^4.32.0", + "@taiga-ui/core": "^4.32.0", + "@taiga-ui/kit": "^4.32.0", + "@taiga-ui/layout": "^4.32.0", "@taiga-ui/polymorpheus": "^4.9.0", "rxjs": ">=7.0.0" } }, "node_modules/@taiga-ui/addon-table": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/addon-table/-/addon-table-4.30.0.tgz", - "integrity": "sha512-OdCEwlrMs42Z2pINK1wvNk7OZmAlkj+mbgHTyMGdrUdA49dFZfYXNpVUCwVOqHAm2PDOeVN4ybZ8FSbzYefJyw==", + "version": "4.32.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/addon-table/-/addon-table-4.32.0.tgz", + "integrity": "sha512-8oXeqLO1wGH8RYHTYWhjCvrKWptPN1we04NRahmFY4AxSJ3u7MqaR4420RRNO4zZG9kGyktLXPjqGocMoymL8Q==", "license": "Apache-2.0", "dependencies": { "tslib": ">=2.8.1" @@ -4490,19 +4490,19 @@ "peerDependencies": { "@angular/common": ">=16.0.0", "@angular/core": ">=16.0.0", - "@ng-web-apis/intersection-observer": "^4.11.1", - "@taiga-ui/cdk": "^4.30.0", - "@taiga-ui/core": "^4.30.0", - "@taiga-ui/i18n": "^4.30.0", - "@taiga-ui/kit": "^4.30.0", + "@ng-web-apis/intersection-observer": "^4.12.0", + "@taiga-ui/cdk": "^4.32.0", + "@taiga-ui/core": "^4.32.0", + "@taiga-ui/i18n": "^4.32.0", + "@taiga-ui/kit": "^4.32.0", "@taiga-ui/polymorpheus": "^4.9.0", "rxjs": ">=7.0.0" } }, "node_modules/@taiga-ui/cdk": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-4.30.0.tgz", - "integrity": "sha512-ndfnLOnL6vriItm5lq8/0slzj03CatkGVYG8zAT3fx00Vuam5Wf8Sh6h2ObqCFAljT7WJxHqMF9A1cBfLPI/iQ==", + "version": "4.32.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-4.32.0.tgz", + "integrity": "sha512-qvYe79uh6Tw2LJSEGLJYUlAidbZi6JgcuMRqWAB1JhyIGpgnaqar5v+oJJg28zJZZ81PCj59VkFNLL0dNVXRUg==", "license": "Apache-2.0", "dependencies": { "tslib": "2.8.1" @@ -4520,20 +4520,20 @@ "@angular/common": ">=16.0.0", "@angular/core": ">=16.0.0", "@angular/forms": ">=16.0.0", - "@ng-web-apis/common": "^4.11.1", - "@ng-web-apis/mutation-observer": "^4.11.1", - "@ng-web-apis/platform": "^4.11.1", - "@ng-web-apis/resize-observer": "^4.11.1", - "@ng-web-apis/screen-orientation": "^4.11.1", - "@taiga-ui/event-plugins": "^4.4.1", + "@ng-web-apis/common": "^4.12.0", + "@ng-web-apis/mutation-observer": "^4.12.0", + "@ng-web-apis/platform": "^4.12.0", + "@ng-web-apis/resize-observer": "^4.12.0", + "@ng-web-apis/screen-orientation": "^4.12.0", + "@taiga-ui/event-plugins": "^4.5.1", "@taiga-ui/polymorpheus": "^4.9.0", "rxjs": ">=7.0.0" } }, "node_modules/@taiga-ui/core": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-4.30.0.tgz", - "integrity": "sha512-IeZ6QBpSuv7k4bQx2BSDr8N3dDiMDwgnnwkkKqtJ0yJayZ/ZlCMq3nUQA0kg3VjH2spJeUbdqkDqpEuzrWJGkA==", + "version": "4.32.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-4.32.0.tgz", + "integrity": "sha512-e1z7YhhjePMRLTk+s83OclN45wMixCwZWMxM9WuXIyd2KXMPhJvrrgBDjoK66GuFtjZ4qaSF/H2FTIJmJ/6MiQ==", "license": "Apache-2.0", "dependencies": { "tslib": ">=2.8.1" @@ -4545,19 +4545,19 @@ "@angular/forms": ">=16.0.0", "@angular/platform-browser": ">=16.0.0", "@angular/router": ">=16.0.0", - "@ng-web-apis/common": "^4.11.1", - "@ng-web-apis/mutation-observer": "^4.11.1", - "@taiga-ui/cdk": "^4.30.0", - "@taiga-ui/event-plugins": "^4.4.1", - "@taiga-ui/i18n": "^4.30.0", + "@ng-web-apis/common": "^4.12.0", + "@ng-web-apis/mutation-observer": "^4.12.0", + "@taiga-ui/cdk": "^4.32.0", + "@taiga-ui/event-plugins": "^4.5.1", + "@taiga-ui/i18n": "^4.32.0", "@taiga-ui/polymorpheus": "^4.9.0", "rxjs": ">=7.0.0" } }, "node_modules/@taiga-ui/event-plugins": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/event-plugins/-/event-plugins-4.5.0.tgz", - "integrity": "sha512-bMW36eqr4Q+EnUM8ZNjx1Sw8POIAcyALY74xVPq9UHoQ3NqnRkeEDnZdfPhq9IYxtC3sO2BttNjWYcvBAkU2+A==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@taiga-ui/event-plugins/-/event-plugins-4.5.1.tgz", + "integrity": "sha512-p5TAs6ZAJAlyl64OUdvnVnfCvDteJtLl9cjXarMzPRY7sX2d+SC97qnTZF8ACPcx4FgFaiHI6VH5G6UzYHMZ8g==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.3.0" @@ -4569,9 +4569,9 @@ } }, "node_modules/@taiga-ui/experimental": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/experimental/-/experimental-4.30.0.tgz", - "integrity": "sha512-0GWkBinW+tqQIFkWQbTqMBTkKGZhju3RslKRCYbjal/hfcIuSAsEPZqLqIQqVqJNz6AhaIpT0UQ+I7QXzx1/yw==", + "version": "4.32.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/experimental/-/experimental-4.32.0.tgz", + "integrity": "sha512-sCOasTF9UlgPOW4vXSeM5M1tgGrjgofa+Qq7hejYW3BXs/4mnmdm5yiYzWfMVZd4jgTSeV5kobkIJ9Fkp2zt6g==", "license": "Apache-2.0", "dependencies": { "tslib": ">=2.8.1" @@ -4579,18 +4579,18 @@ "peerDependencies": { "@angular/common": ">=16.0.0", "@angular/core": ">=16.0.0", - "@taiga-ui/addon-commerce": "^4.30.0", - "@taiga-ui/cdk": "^4.30.0", - "@taiga-ui/core": "^4.30.0", - "@taiga-ui/kit": "^4.30.0", + "@taiga-ui/addon-commerce": "^4.32.0", + "@taiga-ui/cdk": "^4.32.0", + "@taiga-ui/core": "^4.32.0", + "@taiga-ui/kit": "^4.32.0", "@taiga-ui/polymorpheus": "^4.9.0", "rxjs": ">=7.0.0" } }, "node_modules/@taiga-ui/i18n": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-4.30.0.tgz", - "integrity": "sha512-OvtUqSRQE988XfiH1MS7Wd3Eg6dE1mkP2sqYRLw0HyE5Oc9hgHMwdPstSaoMN9aeJRVZnKXGsYmX4iaQ3x7drw==", + "version": "4.32.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-4.32.0.tgz", + "integrity": "sha512-PAQv9RxSgvf3RBUps9bXX2erCk9oiSt9ApM3SAIa/OuzET0TJsW6yZ4EQrtLw03bMX3wyA8PnEYva9wzoYAqxA==", "license": "Apache-2.0", "peer": true, "dependencies": { @@ -4598,23 +4598,23 @@ }, "peerDependencies": { "@angular/core": ">=16.0.0", - "@ng-web-apis/common": "^4.11.1", + "@ng-web-apis/common": "^4.12.0", "rxjs": ">=7.0.0" } }, "node_modules/@taiga-ui/icons": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/icons/-/icons-4.30.0.tgz", - "integrity": "sha512-EAbvw1ii4UVDgt9+5t7NQkV0WBqkVm5SGixH0ux8Vb4qhhLJJwp5xvXOCGt5QPzviT7nFGqXD6EqB23aYcuusg==", + "version": "4.32.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/icons/-/icons-4.32.0.tgz", + "integrity": "sha512-X2ZSiqeMKigULgX91fBZkFJRUbwzeW934yLEGhq7C1JMcC2+ppLmL/NbkD2kpKZ4OeHnGsItxKauoXu44rXeLA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.3.0" } }, "node_modules/@taiga-ui/kit": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-4.30.0.tgz", - "integrity": "sha512-tCHZbsiq1u19ariarFuP9iwnNSxJGicQnYvJYy2+QojL65KsC9p8VgZv36rpggpuPEUXRXwmhyz2Qi6fwFcbLg==", + "version": "4.32.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-4.32.0.tgz", + "integrity": "sha512-J8XoqeQHBNbAAuTz0kVACujDOb3zuh4Vps83lYl+msFIaUmkjC37muXF3eRlImH3m4DpT8yI8+ffh/T3+jky7w==", "license": "Apache-2.0", "dependencies": { "tslib": ">=2.8.1" @@ -4628,21 +4628,21 @@ "@maskito/core": "^3.5.0", "@maskito/kit": "^3.5.0", "@maskito/phone": "^3.5.0", - "@ng-web-apis/common": "^4.11.1", - "@ng-web-apis/intersection-observer": "^4.11.1", - "@ng-web-apis/mutation-observer": "^4.11.1", - "@ng-web-apis/resize-observer": "^4.11.1", - "@taiga-ui/cdk": "^4.30.0", - "@taiga-ui/core": "^4.30.0", - "@taiga-ui/i18n": "^4.30.0", + "@ng-web-apis/common": "^4.12.0", + "@ng-web-apis/intersection-observer": "^4.12.0", + "@ng-web-apis/mutation-observer": "^4.12.0", + "@ng-web-apis/resize-observer": "^4.12.0", + "@taiga-ui/cdk": "^4.32.0", + "@taiga-ui/core": "^4.32.0", + "@taiga-ui/i18n": "^4.32.0", "@taiga-ui/polymorpheus": "^4.9.0", "rxjs": ">=7.0.0" } }, "node_modules/@taiga-ui/layout": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/layout/-/layout-4.30.0.tgz", - "integrity": "sha512-DyIqpmXcv/OP4byt7L1f1iBKPysf3L+sj/dBpkeYvAUUnJnXnJsXav0j57d43VkXPn9lpGqz0gEBtzVDt7xxTw==", + "version": "4.32.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/layout/-/layout-4.32.0.tgz", + "integrity": "sha512-ECaoJ3CbL+eoqL1MleaHvD9/FQ5OCaUMkjOdXId2Jg2MNbuDhtS9hqVZvSWLXRWz3XgC3aADYnPwrNvIsy5Mng==", "license": "Apache-2.0", "dependencies": { "tslib": ">=2.8.1" @@ -4650,17 +4650,17 @@ "peerDependencies": { "@angular/common": ">=16.0.0", "@angular/core": ">=16.0.0", - "@taiga-ui/cdk": "^4.30.0", - "@taiga-ui/core": "^4.30.0", - "@taiga-ui/kit": "^4.30.0", + "@taiga-ui/cdk": "^4.32.0", + "@taiga-ui/core": "^4.32.0", + "@taiga-ui/kit": "^4.32.0", "@taiga-ui/polymorpheus": "^4.9.0", "rxjs": ">=7.0.0" } }, "node_modules/@taiga-ui/legacy": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/legacy/-/legacy-4.30.0.tgz", - "integrity": "sha512-ebFJMddzlsq3TUAWxopn5Qju4REkC4bHzoYYx5OEzPq1VW1zmCvNC+X6usMnluhc9aS50UI8ZB7Xd3N4Zdgtfg==", + "version": "4.32.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/legacy/-/legacy-4.32.0.tgz", + "integrity": "sha512-wEsywt6hK2NNpHddqVrL0MTd1QFzmhMdPPgtraNOieQmzrSW2jpA37KJO11cVleuRdDsk98rFtzQ3stlNNFy5Q==", "license": "Apache-2.0", "dependencies": { "tslib": ">=2.8.1" diff --git a/web/package.json b/web/package.json index f370b37bf..872975986 100644 --- a/web/package.json +++ b/web/package.json @@ -47,18 +47,18 @@ "@noble/hashes": "^1.4.0", "@start9labs/argon2": "^0.2.2", "@start9labs/start-sdk": "file:../sdk/baseDist", - "@taiga-ui/addon-charts": "4.30.0", - "@taiga-ui/addon-commerce": "4.30.0", - "@taiga-ui/addon-mobile": "4.30.0", - "@taiga-ui/addon-table": "4.30.0", - "@taiga-ui/cdk": "4.30.0", - "@taiga-ui/core": "4.30.0", - "@taiga-ui/event-plugins": "4.5.0", - "@taiga-ui/experimental": "4.30.0", - "@taiga-ui/icons": "4.30.0", - "@taiga-ui/kit": "4.30.0", - "@taiga-ui/layout": "4.30.0", - "@taiga-ui/legacy": "4.30.0", + "@taiga-ui/addon-charts": "4.32.0", + "@taiga-ui/addon-commerce": "4.32.0", + "@taiga-ui/addon-mobile": "4.32.0", + "@taiga-ui/addon-table": "4.32.0", + "@taiga-ui/cdk": "4.32.0", + "@taiga-ui/core": "4.32.0", + "@taiga-ui/event-plugins": "4.5.1", + "@taiga-ui/experimental": "4.32.0", + "@taiga-ui/icons": "4.32.0", + "@taiga-ui/kit": "4.32.0", + "@taiga-ui/layout": "4.32.0", + "@taiga-ui/legacy": "4.32.0", "@taiga-ui/polymorpheus": "4.9.0", "@tinkoff/ng-dompurify": "4.0.0", "ansi-to-html": "^0.7.2", diff --git a/web/projects/marketplace/src/components/menu/menu.component.scss b/web/projects/marketplace/src/components/menu/menu.component.scss index 4b4436c10..d4ab0817f 100644 --- a/web/projects/marketplace/src/components/menu/menu.component.scss +++ b/web/projects/marketplace/src/components/menu/menu.component.scss @@ -8,7 +8,7 @@ header { @include scrollbar-hidden(); - // TODO: Theme + // @TODO Theme background: #2b2b2f; overflow: hidden; width: 100%; diff --git a/web/projects/marketplace/src/pages/list/item/item.component.html b/web/projects/marketplace/src/pages/list/item/item.component.html index a46b00f9c..4b3d5fb26 100644 --- a/web/projects/marketplace/src/pages/list/item/item.component.html +++ b/web/projects/marketplace/src/pages/list/item/item.component.html @@ -16,21 +16,3 @@ - - - diff --git a/web/projects/marketplace/src/pages/show/about/about.component.ts b/web/projects/marketplace/src/pages/show/about/about.component.ts index 518cbbf24..ae146050f 100644 --- a/web/projects/marketplace/src/pages/show/about/about.component.ts +++ b/web/projects/marketplace/src/pages/show/about/about.component.ts @@ -6,7 +6,7 @@ import { } from '@angular/core' import { TuiDialogService } from '@taiga-ui/core' import { RELEASE_NOTES } from '../../../modals/release-notes.component' -import { MarketplacePkg } from '../../../types' +import { MarketplacePkgBase } from '../../../types' @Component({ selector: 'marketplace-about', @@ -18,7 +18,7 @@ export class AboutComponent { private readonly dialogs = inject(TuiDialogService) @Input({ required: true }) - pkg!: MarketplacePkg + pkg!: MarketplacePkgBase async onPast() { this.dialogs diff --git a/web/projects/marketplace/src/pages/show/additional/additional-item.component.ts b/web/projects/marketplace/src/pages/show/additional/additional-item.component.ts index aed092d9d..b25950f52 100644 --- a/web/projects/marketplace/src/pages/show/additional/additional-item.component.ts +++ b/web/projects/marketplace/src/pages/show/additional/additional-item.component.ts @@ -1,6 +1,6 @@ import { CommonModule } from '@angular/common' import { ChangeDetectionStrategy, Component, Input } from '@angular/core' -import { TuiIcon, TuiLabel, TuiTitle } from '@taiga-ui/core' +import { TuiIcon, TuiTitle } from '@taiga-ui/core' import { TuiLineClamp } from '@taiga-ui/kit' @Component({ @@ -43,7 +43,7 @@ import { TuiLineClamp } from '@taiga-ui/kit' ], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [CommonModule, TuiLineClamp, TuiLabel, TuiIcon, TuiTitle], + imports: [CommonModule, TuiLineClamp, TuiIcon, TuiTitle], }) export class MarketplaceAdditionalItemComponent { @Input({ required: true }) diff --git a/web/projects/marketplace/src/pages/show/additional/additional.component.html b/web/projects/marketplace/src/pages/show/additional/additional.component.html index f4381eed6..4f1d5b6da 100644 --- a/web/projects/marketplace/src/pages/show/additional/additional.component.html +++ b/web/projects/marketplace/src/pages/show/additional/additional.component.html @@ -4,7 +4,7 @@
- diff --git a/web/projects/marketplace/src/pages/show/additional/additional.component.ts b/web/projects/marketplace/src/pages/show/additional/additional.component.ts index 7e9b6c3ff..67914f280 100644 --- a/web/projects/marketplace/src/pages/show/additional/additional.component.ts +++ b/web/projects/marketplace/src/pages/show/additional/additional.component.ts @@ -7,8 +7,7 @@ import { } from '@angular/core' import { ActivatedRoute } from '@angular/router' import { CopyService } from '@start9labs/shared' -import { TuiDialogService } from '@taiga-ui/core' -import { MarketplacePkg } from '../../../types' +import { MarketplacePkgBase } from '../../../types' @Component({ selector: 'marketplace-additional', @@ -18,14 +17,13 @@ import { MarketplacePkg } from '../../../types' }) export class AdditionalComponent { @Input({ required: true }) - pkg!: MarketplacePkg + pkg!: MarketplacePkgBase @Output() - readonly static = new EventEmitter() + readonly static = new EventEmitter<'License' | 'Instructions'>() constructor( readonly copyService: CopyService, - private readonly dialogs: TuiDialogService, private readonly route: ActivatedRoute, ) {} diff --git a/web/projects/marketplace/src/pages/show/dependencies/dependencies.component.ts b/web/projects/marketplace/src/pages/show/dependencies/dependencies.component.ts index 8f1b6b880..c6534a8f2 100644 --- a/web/projects/marketplace/src/pages/show/dependencies/dependencies.component.ts +++ b/web/projects/marketplace/src/pages/show/dependencies/dependencies.component.ts @@ -6,7 +6,7 @@ import { Input, Output, } from '@angular/core' -import { MarketplacePkg } from '../../../types' +import { MarketplacePkgBase } from '../../../types' import { MarketplaceDepItemComponent } from './dependency-item.component' @Component({ @@ -55,7 +55,7 @@ import { MarketplaceDepItemComponent } from './dependency-item.component' }) export class MarketplaceDependenciesComponent { @Input({ required: true }) - pkg!: MarketplacePkg + pkg!: MarketplacePkgBase @Output() open = new EventEmitter() } diff --git a/web/projects/marketplace/src/pages/show/dependencies/dependency-item.component.ts b/web/projects/marketplace/src/pages/show/dependencies/dependency-item.component.ts index 171ff1051..bbae9f295 100644 --- a/web/projects/marketplace/src/pages/show/dependencies/dependency-item.component.ts +++ b/web/projects/marketplace/src/pages/show/dependencies/dependency-item.component.ts @@ -3,9 +3,8 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { RouterModule } from '@angular/router' import { ExverPipesModule } from '@start9labs/shared' import { T } from '@start9labs/start-sdk' -import { TuiLet } from '@taiga-ui/cdk' import { TuiAvatar, TuiLineClamp } from '@taiga-ui/kit' -import { MarketplacePkg } from '../../../types' +import { MarketplacePkgBase } from '../../../types' @Component({ selector: 'marketplace-dep-item', @@ -97,12 +96,11 @@ import { MarketplacePkg } from '../../../types' TuiAvatar, ExverPipesModule, TuiLineClamp, - TuiLet, ], }) export class MarketplaceDepItemComponent { @Input({ required: true }) - pkg!: MarketplacePkg + pkg!: MarketplacePkgBase @Input({ required: true }) dep!: KeyValue diff --git a/web/projects/marketplace/src/types.ts b/web/projects/marketplace/src/types.ts index e5022cc68..6f24fcea3 100644 --- a/web/projects/marketplace/src/types.ts +++ b/web/projects/marketplace/src/types.ts @@ -1,3 +1,4 @@ +import { OptionalProperty } from '@start9labs/shared' import { T } from '@start9labs/start-sdk' export type GetPackageReq = { @@ -31,11 +32,17 @@ export type StoreData = { packages: MarketplacePkg[] } -export type MarketplacePkg = T.PackageVersionInfo & - Omit & { - id: T.PackageId - version: string - flavor: string | null - } +export type MarketplacePkgBase = OptionalProperty< + T.PackageVersionInfo, + 's9pk' +> & { + id: T.PackageId + version: string + flavor: string | null +} + +export type MarketplacePkg = MarketplacePkgBase & + GetPackageRes & + T.PackageVersionInfo export type StoreDataWithUrl = StoreData & { url: string } diff --git a/web/projects/setup-wizard/src/app/services/mock-api.service.ts b/web/projects/setup-wizard/src/app/services/mock-api.service.ts index 0943cd466..2f5a52ea1 100644 --- a/web/projects/setup-wizard/src/app/services/mock-api.service.ts +++ b/web/projects/setup-wizard/src/app/services/mock-api.service.ts @@ -117,7 +117,7 @@ export class MockApiService extends ApiService { })), ) as Observable } else if (guid === 'progress-guid') { - // @TODO mock progress + // @TODO Matt mock progress return interval(1000).pipe( map(() => ({ overall: true, diff --git a/web/projects/shared/src/components/initializing/initializing.component.ts b/web/projects/shared/src/components/initializing/initializing.component.ts index fd471cbb1..470bc6759 100644 --- a/web/projects/shared/src/components/initializing/initializing.component.ts +++ b/web/projects/shared/src/components/initializing/initializing.component.ts @@ -1,6 +1,5 @@ import { CommonModule } from '@angular/common' import { ChangeDetectionStrategy, Component, Input } from '@angular/core' -import { TuiLet } from '@taiga-ui/cdk' import { TuiProgress } from '@taiga-ui/kit' import { LogsWindowComponent } from './logs-window.component' @@ -31,7 +30,7 @@ import { LogsWindowComponent } from './logs-window.component' padding: 1rem; margin: 1.5rem; text-align: center; - /* TODO: Theme */ + // @TODO Theme background: #e0e0e0; color: #333; --tui-background-neutral-1: rgba(0, 0, 0, 0.1); @@ -46,11 +45,11 @@ import { LogsWindowComponent } from './logs-window.component' text-align: left; overflow: hidden; border-radius: 2rem; - /* TODO: Theme */ + // @TODO Theme background: #181818; } `, - imports: [CommonModule, LogsWindowComponent, TuiLet, TuiProgress], + imports: [CommonModule, LogsWindowComponent, TuiProgress], changeDetection: ChangeDetectionStrategy.OnPush, }) export class InitializingComponent { diff --git a/web/projects/shared/src/directives/safe-links.directive.ts b/web/projects/shared/src/directives/safe-links.directive.ts index 07b794b92..c946e101c 100644 --- a/web/projects/shared/src/directives/safe-links.directive.ts +++ b/web/projects/shared/src/directives/safe-links.directive.ts @@ -1,7 +1,7 @@ import { AfterViewInit, Directive, ElementRef, Inject } from '@angular/core' import { DOCUMENT } from '@angular/common' -// TODO: Refactor to use `MutationObserver` so it works with dynamic content +// @TODO Alex: Refactor to use `MutationObserver` so it works with dynamic content @Directive({ selector: '[safeLinks]', standalone: true, diff --git a/web/projects/shared/src/services/error.service.ts b/web/projects/shared/src/services/error.service.ts index 0e7c7ce05..fa5394b11 100644 --- a/web/projects/shared/src/services/error.service.ts +++ b/web/projects/shared/src/services/error.service.ts @@ -2,7 +2,7 @@ import { ErrorHandler, inject, Injectable } from '@angular/core' import { TuiAlertService } from '@taiga-ui/core' import { HttpError } from '../classes/http-error' -// TODO: Enable this as ErrorHandler +// @TODO Alex: Enable this as ErrorHandler @Injectable({ providedIn: 'root', }) diff --git a/web/projects/shared/src/services/setup-logs.service.ts b/web/projects/shared/src/services/setup-logs.service.ts index a142ef8c0..245a26067 100644 --- a/web/projects/shared/src/services/setup-logs.service.ts +++ b/web/projects/shared/src/services/setup-logs.service.ts @@ -1,5 +1,13 @@ import { StaticClassProvider } from '@angular/core' -import { bufferTime, defer, map, Observable, scan, switchMap } from 'rxjs' +import { + bufferTime, + defer, + filter, + map, + Observable, + scan, + switchMap, +} from 'rxjs' import { FollowLogsReq, FollowLogsRes, Log } from '../types/api' import { Constructor } from '../types/constructor' import { convertAnsi } from '../util/convert-ansi' @@ -22,7 +30,8 @@ export function provideSetupLogsService( export class SetupLogsService extends Observable { private readonly log$ = defer(() => this.api.followServerLogs({})).pipe( switchMap(({ guid }) => this.api.openWebsocket$(guid)), - bufferTime(1000), + bufferTime(500), + filter(logs => !!logs.length), map(convertAnsi), scan((logs: readonly string[], log) => [...logs, log], []), ) diff --git a/web/projects/shared/src/util/format-progress.ts b/web/projects/shared/src/util/format-progress.ts index a625e57f4..9a6a1401e 100644 --- a/web/projects/shared/src/util/format-progress.ts +++ b/web/projects/shared/src/util/format-progress.ts @@ -1,22 +1,29 @@ -// @TODO get types from sdk -type Progress = null | boolean | { done: number; total: number | null } -type NamedProgress = { name: string; progress: Progress } -type FullProgress = { overall: Progress; phases: Array } +import { T } from '@start9labs/start-sdk' -export function formatProgress({ phases, overall }: FullProgress): { +export function formatProgress({ phases, overall }: T.FullProgress): { total: number message: string } { return { total: getDecimal(overall), message: phases - .filter(p => p.progress !== true && p.progress !== null) - .map(p => `${p.name}${getPhaseBytes(p.progress)}`) + .filter( + ( + p, + ): p is { + name: string + progress: { + done: number + total: number | null + } + } => p.progress !== true && p.progress !== null, + ) + .map(p => `${p.name}${getPhaseBytes(p.progress)}`) .join(', '), } } -function getDecimal(progress: Progress): number { +function getDecimal(progress: T.Progress): number { if (progress === true) { return 1 } else if (!progress || !progress.total) { @@ -26,7 +33,7 @@ function getDecimal(progress: Progress): number { } } -function getPhaseBytes(progress: Progress): string { +function getPhaseBytes(progress: T.Progress): string { return progress === true || !progress ? '' : `: (${progress.done}/${progress.total})` diff --git a/web/projects/shared/src/util/misc.util.ts b/web/projects/shared/src/util/misc.util.ts index b79411daf..21a3c32f3 100644 --- a/web/projects/shared/src/util/misc.util.ts +++ b/web/projects/shared/src/util/misc.util.ts @@ -55,3 +55,6 @@ export function toUrl(text: string | null | undefined): string { } export type WithId = T & { id: string } + +export type OptionalProperty = Omit & + Partial> diff --git a/web/projects/shared/styles/shared.scss b/web/projects/shared/styles/shared.scss index 1d38a6f02..b4be91ecc 100644 --- a/web/projects/shared/styles/shared.scss +++ b/web/projects/shared/styles/shared.scss @@ -169,7 +169,7 @@ $wide-modal: 900px; --portal-header-height: 56px; - // @TODO rename when make style lib + // @TODO Alex rename when make style lib --tw-color-black: 0 0 0; --tw-color-white: 255 255 255; --tw-color-slate-50: 248 250 252; diff --git a/web/projects/shared/styles/taiga.scss b/web/projects/shared/styles/taiga.scss index 68b4b97ca..2a5893db5 100644 --- a/web/projects/shared/styles/taiga.scss +++ b/web/projects/shared/styles/taiga.scss @@ -139,7 +139,7 @@ tui-dropdown[data-appearance='start-os'][data-appearance='start-os'] { background: rgb(34 34 34 / 80%); } -// TODO: Move to Taiga UI +// @TODO Alex: Move to Taiga UI a[tuiIconButton]:not([href]) { pointer-events: none; opacity: var(--tui-disabled-opacity); diff --git a/web/projects/ui/src/app/components/refresh-alert.component.ts b/web/projects/ui/src/app/components/refresh-alert.component.ts index f591f79f1..1624214f7 100644 --- a/web/projects/ui/src/app/components/refresh-alert.component.ts +++ b/web/projects/ui/src/app/components/refresh-alert.component.ts @@ -9,6 +9,7 @@ import { debounceTime, endWith, map, merge, Subject } from 'rxjs' import { ConfigService } from 'src/app/services/config.service' import { DataModel } from 'src/app/services/patch-db/data-model' +// @TODO Alex @Component({ standalone: true, selector: 'refresh-alert', diff --git a/web/projects/ui/src/app/routes/initializing/initializing.page.ts b/web/projects/ui/src/app/routes/initializing/initializing.page.ts index 5a2a85bfa..be48a1dbf 100644 --- a/web/projects/ui/src/app/routes/initializing/initializing.page.ts +++ b/web/projects/ui/src/app/routes/initializing/initializing.page.ts @@ -6,16 +6,7 @@ import { provideSetupLogsService, } from '@start9labs/shared' import { T } from '@start9labs/start-sdk' -import { - catchError, - defer, - EMPTY, - from, - map, - startWith, - switchMap, - tap, -} from 'rxjs' +import { catchError, defer, from, map, startWith, switchMap, tap } from 'rxjs' import { ApiService } from 'src/app/services/api/embassy-api.service' import { StateService } from 'src/app/services/state.service' @@ -36,18 +27,25 @@ export default class InitializingPage { defer(() => from(this.api.initFollowProgress())).pipe( switchMap(({ guid, progress }) => this.api - .openWebsocket$(guid, {}) + .openWebsocket$(guid, { + closeObserver: { + next: () => { + this.state.syncState() + }, + }, + }) .pipe(startWith(progress)), ), map(formatProgress), - tap<{ total: number; message: string }>(({ total }) => { + tap(({ total }) => { if (total === 1) { this.state.syncState() } }), - catchError(e => { + catchError((e, caught$) => { console.error(e) - return EMPTY + this.state.syncState() + return caught$ }), ), { initialValue: { total: 0, message: '' } }, diff --git a/web/projects/ui/src/app/routes/login/login.page.ts b/web/projects/ui/src/app/routes/login/login.page.ts index 92acb839e..5456b1104 100644 --- a/web/projects/ui/src/app/routes/login/login.page.ts +++ b/web/projects/ui/src/app/routes/login/login.page.ts @@ -42,7 +42,6 @@ export class LoginPage { } await this.api.login({ password: this.password, - metadata: { platforms: [] }, // @TODO do we really need platforms now? ephemeral: window.location.host === 'localhost', }) diff --git a/web/projects/ui/src/app/routes/portal/components/form/control.ts b/web/projects/ui/src/app/routes/portal/components/form/control.ts index 5cf9d84ab..91ad64489 100644 --- a/web/projects/ui/src/app/routes/portal/components/form/control.ts +++ b/web/projects/ui/src/app/routes/portal/components/form/control.ts @@ -17,7 +17,7 @@ export abstract class Control< return this.control.spec } - // TODO: Properly handle already set immutable value + // @TODO Alex: Properly handle already set immutable value get readOnly(): boolean { return ( !!this.value && !!this.control.control?.pristine && this.control.immutable diff --git a/web/projects/ui/src/app/routes/portal/components/header/header.component.ts b/web/projects/ui/src/app/routes/portal/components/header/header.component.ts index aa44c1bec..0b50916d1 100644 --- a/web/projects/ui/src/app/routes/portal/components/header/header.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/header/header.component.ts @@ -91,7 +91,7 @@ import { HeaderStatusComponent } from './status.component' } &:has([data-status='success']) { - --status: var(--tui-status-positive); + --status: transparent; } } diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/actions.component.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/actions.component.ts index 3c35544b1..692435a63 100644 --- a/web/projects/ui/src/app/routes/portal/components/interfaces/actions.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/interfaces/actions.component.ts @@ -88,6 +88,7 @@ import { InterfaceComponent } from './interface.component' text-align: right; grid-area: 1 / 2 / 3 / 3; place-content: center; + white-space: nowrap; } .mobile { diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/interface.component.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/interface.component.ts index 42ff76a6f..0eb8e7d38 100644 --- a/web/projects/ui/src/app/routes/portal/components/interfaces/interface.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/interfaces/interface.component.ts @@ -15,6 +15,7 @@ import { MappedServiceInterface } from './interface.utils' `, styles: ` :host { + max-width: 56rem; display: flex; flex-direction: column; gap: 1rem; diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/status.component.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/status.component.ts new file mode 100644 index 000000000..f550e63c0 --- /dev/null +++ b/web/projects/ui/src/app/routes/portal/components/interfaces/status.component.ts @@ -0,0 +1,23 @@ +import { ChangeDetectionStrategy, Component, input } from '@angular/core' +import { TuiBadge } from '@taiga-ui/kit' + +@Component({ + standalone: true, + selector: 'interface-status', + template: ` + + {{ public() ? 'Public' : 'Private' }} + + `, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [TuiBadge], +}) +export class InterfaceStatusComponent { + readonly public = input(false) +} diff --git a/web/projects/ui/src/app/routes/portal/components/tabs.component.ts b/web/projects/ui/src/app/routes/portal/components/tabs.component.ts index a34e8f454..0b51d649e 100644 --- a/web/projects/ui/src/app/routes/portal/components/tabs.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/tabs.component.ts @@ -80,7 +80,7 @@ const FILTER = ['/portal/services', '/portal/system', '/portal/marketplace'] :host { display: none; backdrop-filter: blur(1rem); - // TODO: Theme + // TODO Theme --tui-background-elevation-1: #333; --tui-background-base: #fff; --tui-border-normal: var(--tui-background-neutral-1); diff --git a/web/projects/ui/src/app/routes/portal/portal.component.ts b/web/projects/ui/src/app/routes/portal/portal.component.ts index 67a592020..c7275e75e 100644 --- a/web/projects/ui/src/app/routes/portal/portal.component.ts +++ b/web/projects/ui/src/app/routes/portal/portal.component.ts @@ -24,7 +24,7 @@ import { HeaderComponent } from './components/header/header.component' height: 100%; display: flex; flex-direction: column; - // TODO: Theme + // @TODO Theme background: url(/assets/img/background_dark.jpeg) fixed center/cover; } diff --git a/web/projects/ui/src/app/routes/portal/routes/backups/components/upcoming.component.ts b/web/projects/ui/src/app/routes/portal/routes/backups/components/upcoming.component.ts index 385ee3c5f..b2bb1f3e8 100644 --- a/web/projects/ui/src/app/routes/portal/routes/backups/components/upcoming.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/backups/components/upcoming.component.ts @@ -102,7 +102,7 @@ export class BackupsUpcomingComponent { readonly targets = toSignal(from(this.api.getBackupTargets({}))) readonly current = toSignal( inject>(PatchDB) - // @TODO remove "as any" once this feature is real + // @TODO 041 remove "as any" once this feature is real .watch$('serverInfo', 'statusInfo', 'currentBackup' as any, 'job') .pipe(map(job => job || {})), ) diff --git a/web/projects/ui/src/app/routes/portal/routes/marketplace/components/controls.component.ts b/web/projects/ui/src/app/routes/portal/routes/marketplace/components/controls.component.ts index 3de00b299..697a129a8 100644 --- a/web/projects/ui/src/app/routes/portal/routes/marketplace/components/controls.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/marketplace/components/controls.component.ts @@ -7,7 +7,7 @@ import { Input, } from '@angular/core' import { Router } from '@angular/router' -import { MarketplacePkg } from '@start9labs/marketplace' +import { MarketplacePkgBase } from '@start9labs/marketplace' import { Exver, ErrorService, @@ -103,7 +103,7 @@ export class MarketplaceControlsComponent { private readonly marketplaceService = inject(MarketplaceService) @Input({ required: true }) - pkg!: MarketplacePkg + pkg!: MarketplacePkgBase @Input() localPkg!: PackageDataEntry | null diff --git a/web/projects/ui/src/app/routes/portal/routes/marketplace/modals/preview.component.ts b/web/projects/ui/src/app/routes/portal/routes/marketplace/modals/preview.component.ts index 751a8ccff..349db4bdd 100644 --- a/web/projects/ui/src/app/routes/portal/routes/marketplace/modals/preview.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/marketplace/modals/preview.component.ts @@ -22,10 +22,9 @@ import { TuiButton, TuiDialogContext, TuiDialogService, - TuiIcon, TuiLoader, } from '@taiga-ui/core' -import { TuiRadioList, TuiStringifyContentPipe } from '@taiga-ui/kit' +import { TuiRadioList } from '@taiga-ui/kit' import { BehaviorSubject, combineLatest, @@ -53,7 +52,7 @@ import { MarketplaceService } from 'src/app/services/marketplace.service' @if (!(pkg.dependencyMetadata | empty)) { } - + @if (versions$ | async; as versions) { , diff --git a/web/projects/ui/src/app/routes/portal/routes/marketplace/services/alerts.service.ts b/web/projects/ui/src/app/routes/portal/routes/marketplace/services/alerts.service.ts index 9f5348f12..006e794a5 100644 --- a/web/projects/ui/src/app/routes/portal/routes/marketplace/services/alerts.service.ts +++ b/web/projects/ui/src/app/routes/portal/routes/marketplace/services/alerts.service.ts @@ -1,6 +1,6 @@ import { TUI_CONFIRM } from '@taiga-ui/kit' import { inject, Injectable } from '@angular/core' -import { MarketplacePkg } from '@start9labs/marketplace' +import { MarketplacePkg, MarketplacePkgBase } from '@start9labs/marketplace' import { TuiDialogService } from '@taiga-ui/core' import { PatchDB } from 'patch-db-client' import { defaultIfEmpty, firstValueFrom } from 'rxjs' @@ -60,7 +60,7 @@ export class MarketplaceAlertsService { }) } - async alertInstall({ alerts }: MarketplacePkg): Promise { + async alertInstall({ alerts }: MarketplacePkgBase): Promise { const content = alerts.install return ( diff --git a/web/projects/ui/src/app/routes/portal/routes/metrics/metrics.service.ts b/web/projects/ui/src/app/routes/portal/routes/metrics/metrics.service.ts index b14093773..dfc0c33a7 100644 --- a/web/projects/ui/src/app/routes/portal/routes/metrics/metrics.service.ts +++ b/web/projects/ui/src/app/routes/portal/routes/metrics/metrics.service.ts @@ -1,19 +1,27 @@ import { inject, Injectable } from '@angular/core' import { + catchError, defer, + filter, + ignoreElements, Observable, + repeat, retry, shareReplay, startWith, switchMap, + take, + tap, } from 'rxjs' import { ServerMetrics } from 'src/app/services/api/api.types' import { ApiService } from 'src/app/services/api/embassy-api.service' +import { ConnectionService } from 'src/app/services/connection.service' @Injectable({ providedIn: 'root', }) export class MetricsService extends Observable { + private readonly connection = inject(ConnectionService) private readonly api = inject(ApiService) private readonly metrics$ = defer(() => @@ -22,8 +30,10 @@ export class MetricsService extends Observable { switchMap(({ guid, metrics }) => this.api.openWebsocket$(guid).pipe(startWith(metrics)), ), - // @TODO Alex how to handle failure and reconnection here? Simple retry() will not work. Seems like we need a general solution for reconnecting websockets: patchDB, logs, metrics, progress, and any future. Reconnection should depend on server state, then we need to get a new guid, then reconnect. Similar to how patchDB websocket currently behaves on disconnect/reconnect. - retry(), + catchError(() => + this.connection.pipe(filter(Boolean), take(1), ignoreElements()), + ), + repeat(), shareReplay(1), ) diff --git a/web/projects/ui/src/app/routes/portal/routes/metrics/temperature.component.ts b/web/projects/ui/src/app/routes/portal/routes/metrics/temperature.component.ts index 7e7f56e8d..ae324abb7 100644 --- a/web/projects/ui/src/app/routes/portal/routes/metrics/temperature.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/metrics/temperature.component.ts @@ -56,7 +56,7 @@ import { ChangeDetectionStrategy, Component, input } from '@angular/core' :host { height: 100%; - min-height: 7.5rem; + min-height: 9rem; display: flex; flex-direction: column; align-items: center; diff --git a/web/projects/ui/src/app/routes/portal/routes/metrics/time.component.ts b/web/projects/ui/src/app/routes/portal/routes/metrics/time.component.ts index 95e0da4d7..a000d2bd2 100644 --- a/web/projects/ui/src/app/routes/portal/routes/metrics/time.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/metrics/time.component.ts @@ -16,11 +16,6 @@ import { TimeService } from 'src/app/services/time.service' selector: 'metrics-time', template: ` @if (now(); as time) { - @if (!time.synced) { - - - - }
@@ -36,6 +31,11 @@ import { TimeService } from 'src/app/services/time.service' /> }
+ @if (!time.synced) { + + + + } } @else { Loading... } @@ -61,10 +61,12 @@ import { TimeService } from 'src/app/services/time.service' styles: ` :host { height: 100%; + min-height: var(--tui-height-l); display: flex; flex-direction: column; justify-content: center; - gap: 1rem; + align-items: center; + gap: 0.5rem; margin-bottom: 1.5rem; [tuiCell], @@ -72,6 +74,10 @@ import { TimeService } from 'src/app/services/time.service' [tuiSubtitle] { margin: 0; justify-content: center; + + &::after { + display: none; + } } } @@ -79,6 +85,10 @@ import { TimeService } from 'src/app/services/time.service' display: none; } + tui-notification { + width: fit-content; + } + :host-context(tui-root._mobile) { tui-notification { display: none; diff --git a/web/projects/ui/src/app/routes/portal/routes/metrics/uptime.component.ts b/web/projects/ui/src/app/routes/portal/routes/metrics/uptime.component.ts index ca2937c17..7f2428064 100644 --- a/web/projects/ui/src/app/routes/portal/routes/metrics/uptime.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/metrics/uptime.component.ts @@ -30,6 +30,7 @@ import { TimeService } from 'src/app/services/time.service' styles: ` :host { height: 100%; + min-height: var(--tui-height-l); display: flex; text-align: center; justify-content: center; diff --git a/web/projects/ui/src/app/routes/portal/routes/services/components/action.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/components/action.component.ts index 5e97ffd6d..38ba7b416 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/components/action.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/components/action.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { T } from '@start9labs/start-sdk' -import { TuiIcon, TuiTitle } from '@taiga-ui/core' +import { TuiTitle } from '@taiga-ui/core' interface ActionItem { readonly name: string @@ -12,7 +12,6 @@ interface ActionItem { @Component({ selector: '[action]', template: ` -
{{ action.name }}
{{ action.description }}
@@ -23,7 +22,7 @@ interface ActionItem { `, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [TuiIcon, TuiTitle], + imports: [TuiTitle], host: { '[disabled]': '!!disabled', }, diff --git a/web/projects/ui/src/app/routes/portal/routes/services/components/actions.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/components/controls.component.ts similarity index 69% rename from web/projects/ui/src/app/routes/portal/routes/services/components/actions.component.ts rename to web/projects/ui/src/app/routes/portal/routes/services/components/controls.component.ts index eaae1979b..8527f4787 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/components/actions.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/components/controls.component.ts @@ -1,27 +1,27 @@ import { ChangeDetectionStrategy, Component, + computed, inject, Input, } from '@angular/core' -import { T } from '@start9labs/start-sdk' -import { tuiPure } from '@taiga-ui/cdk' import { TuiButton } from '@taiga-ui/core' -import { DependencyInfo } from 'src/app/routes/portal/routes/services/types/dependency-info' +import { map } from 'rxjs' import { ControlsService } from 'src/app/services/controls.service' +import { DepErrorService } from 'src/app/services/dep-error.service' import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { PrimaryStatus } from 'src/app/services/pkg-status-rendering.service' import { getManifest } from 'src/app/utils/get-package-data' @Component({ - selector: 'service-actions', + selector: 'service-controls', template: ` @if (['running', 'starting', 'restarting'].includes(status)) { @@ -31,7 +31,7 @@ import { getManifest } from 'src/app/utils/get-package-data' @@ -41,7 +41,7 @@ import { getManifest } from 'src/app/utils/get-package-data' @@ -78,24 +78,26 @@ import { getManifest } from 'src/app/utils/get-package-data' standalone: true, imports: [TuiButton], }) -export class ServiceActionsComponent { +export class ServiceControlsComponent { + private readonly errors = inject(DepErrorService) + @Input({ required: true }) pkg!: PackageDataEntry @Input({ required: true }) status!: PrimaryStatus - // TODO - dependencies: readonly DependencyInfo[] = [] + readonly manifest = computed(() => getManifest(this.pkg)) - readonly actions = inject(ControlsService) + readonly controls = inject(ControlsService) - get manifest(): T.Manifest { - return getManifest(this.pkg) - } - - @tuiPure - hasUnmet(dependencies: readonly DependencyInfo[]): boolean { - return dependencies.some(dep => !!dep.errorText) - } + readonly hasUnmet = computed(() => + this.errors.getPkgDepErrors$(this.manifest().id).pipe( + map(errors => + Object.keys(this.pkg.currentDependencies) + .map(id => errors[id]) + .some(Boolean), + ), + ), + ) } diff --git a/web/projects/ui/src/app/routes/portal/routes/services/components/interface.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/components/interface.component.ts index a322baa6d..a84bac940 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/components/interface.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/components/interface.component.ts @@ -5,11 +5,11 @@ import { Input, } from '@angular/core' import { RouterLink } from '@angular/router' +import { T } from '@start9labs/start-sdk' import { TuiButton, TuiLink } from '@taiga-ui/core' import { TuiBadge } from '@taiga-ui/kit' import { ConfigService } from 'src/app/services/config.service' import { PackageDataEntry } from 'src/app/services/patch-db/data-model' -import { MappedInterface } from '../types/mapped-interface' @Component({ selector: 'tr[serviceInterface]', @@ -113,7 +113,10 @@ export class ServiceInterfaceComponent { private readonly config = inject(ConfigService) @Input({ required: true }) - info!: MappedInterface + info!: T.ServiceInterface & { + public: boolean + routerLink: string + } @Input({ required: true }) pkg!: PackageDataEntry diff --git a/web/projects/ui/src/app/routes/portal/routes/services/components/status.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/components/status.component.ts index 8117a7d30..0c6adb784 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/components/status.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/components/status.component.ts @@ -1,11 +1,12 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { T } from '@start9labs/start-sdk' import { TuiLoader } from '@taiga-ui/core' +import { getProgressText } from 'src/app/routes/portal/routes/services/pipes/install-progress.pipe' import { InstallingInfo } from 'src/app/services/patch-db/data-model' import { PrimaryRendering, PrimaryStatus, } from 'src/app/services/pkg-status-rendering.service' -import { InstallingProgressDisplayPipe } from '../pipes/install-progress.pipe' @Component({ selector: 'service-status', @@ -17,7 +18,7 @@ import { InstallingProgressDisplayPipe } from '../pipes/install-progress.pipe' Installing - {{ installingInfo.progress.overall | installingProgressString }} + {{ getText(installingInfo.progress.overall) }} } @else {

@@ -84,7 +85,7 @@ import { InstallingProgressDisplayPipe } from '../pipes/install-progress.pipe' host: { class: 'g-card' }, standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [InstallingProgressDisplayPipe, TuiLoader], + imports: [TuiLoader], }) export class ServiceStatusComponent { @Input({ required: true }) @@ -120,4 +121,8 @@ export class ServiceStatusComponent { get rendering() { return PrimaryRendering[this.status] } + + getText(progress: T.Progress): string { + return getProgressText(progress) + } } diff --git a/web/projects/ui/src/app/routes/portal/routes/services/dashboard/dashboard.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/dashboard/dashboard.component.ts index c31e74c38..f134f8e66 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/dashboard/dashboard.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/dashboard/dashboard.component.ts @@ -21,14 +21,7 @@ import { ServicesService } from './services.service' Name Version Uptime - - Status - + Status Controls diff --git a/web/projects/ui/src/app/routes/portal/routes/services/dashboard/status.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/dashboard/status.component.ts index 7abbcaf34..de2a77797 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/dashboard/status.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/dashboard/status.component.ts @@ -1,14 +1,9 @@ -import { - ChangeDetectionStrategy, - Component, - inject, - Input, -} from '@angular/core' +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { tuiPure } from '@taiga-ui/cdk' import { TuiIcon, TuiLoader } from '@taiga-ui/core' +import { getProgressText } from 'src/app/routes/portal/routes/services/pipes/install-progress.pipe' import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service' -import { InstallingProgressDisplayPipe } from '../pipes/install-progress.pipe' @Component({ standalone: true, @@ -31,6 +26,7 @@ import { InstallingProgressDisplayPipe } from '../pipes/install-progress.pipe' align-items: center; gap: 0.5rem; height: 3rem; + white-space: nowrap; } :host-context(tui-root._mobile) { @@ -46,11 +42,8 @@ import { InstallingProgressDisplayPipe } from '../pipes/install-progress.pipe' `, changeDetection: ChangeDetectionStrategy.OnPush, imports: [TuiIcon, TuiLoader], - providers: [InstallingProgressDisplayPipe], }) export class StatusComponent { - private readonly pipe = inject(InstallingProgressDisplayPipe) - @Input() pkg!: PackageDataEntry @@ -72,7 +65,7 @@ export class StatusComponent { get status(): string { if (this.pkg.stateInfo.installingInfo) { - return `Installing...${this.pipe.transform(this.pkg.stateInfo.installingInfo.progress.overall)}` + return `Installing...${getProgressText(this.pkg.stateInfo.installingInfo.progress.overall)}` } switch (this.getStatus(this.pkg).primary) { diff --git a/web/projects/ui/src/app/routes/portal/routes/services/pipes/install-progress.pipe.ts b/web/projects/ui/src/app/routes/portal/routes/services/pipes/install-progress.pipe.ts index 3ae2d7576..6816af944 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/pipes/install-progress.pipe.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/pipes/install-progress.pipe.ts @@ -1,22 +1,6 @@ import { Pipe, PipeTransform } from '@angular/core' import { T } from '@start9labs/start-sdk' -// TODO drop these pipes -@Pipe({ - standalone: true, - name: 'installingProgressString', -}) -export class InstallingProgressDisplayPipe implements PipeTransform { - transform(progress: T.Progress): string { - if (progress === true) return 'finalizing' - if (progress === false || progress === null || !progress.total) - return 'unknown %' - const percentage = Math.round((100 * progress.done) / progress.total) - - return percentage < 99 ? String(percentage) + '%' : 'finalizing' - } -} - @Pipe({ standalone: true, name: 'installingProgress', @@ -28,3 +12,12 @@ export class InstallingProgressPipe implements PipeTransform { return Math.floor((100 * progress.done) / progress.total) } } + +export function getProgressText(progress: T.Progress): string { + if (progress === true) return 'finalizing' + if (!progress || !progress.total) return 'unknown %' + + const percentage = Math.round((100 * progress.done) / progress.total) + + return percentage < 99 ? `${percentage}%` : 'finalizing' +} diff --git a/web/projects/ui/src/app/routes/portal/routes/services/routes/about.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/routes/about.component.ts index 266098d15..d7507f215 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/routes/about.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/routes/about.component.ts @@ -39,7 +39,7 @@ import { section { display: flex; flex-direction: column; - max-width: 32rem; + max-width: 36rem; padding: 0.5rem 1rem; } `, diff --git a/web/projects/ui/src/app/routes/portal/routes/services/routes/actions.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/routes/actions.component.ts index a8276bd2c..291a51335 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/routes/actions.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/routes/actions.component.ts @@ -50,7 +50,7 @@ const OTHER = 'Other Custom Actions' `, styles: ` section { - max-width: 54rem; + max-width: 42rem; display: flex; flex-direction: column; margin-bottom: 2rem; @@ -74,18 +74,20 @@ export default class ServiceActionsRoute { mainStatus: pkg.status.main, icon: pkg.icon, manifest: getManifest(pkg), - actions: Object.keys(pkg.actions).reduce< - Record> - >( - (acc, id) => { - const action = { id, ...pkg.actions[id] } - const group = pkg.actions[id].group || OTHER - const current = acc[group] || [] + actions: Object.entries(pkg.actions) + .filter(([_, val]) => val.visibility !== 'hidden') + .reduce< + Record> + >( + (acc, [id]) => { + const action = { id, ...pkg.actions[id] } + const group = pkg.actions[id].group || OTHER + const current = acc[group] || [] - return { ...acc, [group]: current.concat(action) } - }, - { [OTHER]: [] }, - ), + return { ...acc, [group]: current.concat(action) } + }, + { [OTHER]: [] }, + ), })), ), ) @@ -110,7 +112,7 @@ const REBUILD = { icon: '@tui.wrench', name: 'Rebuild Service', description: - 'Rebuilds the service container. It is harmless and only takes a few seconds to complete, but it should only be necessary if a StartOS bug is preventing dependencies, interfaces, or actions from synchronizing.', + 'Rebuilds the service container. Only necessary in there is a bug in StartOS', } const UNINSTALL = { diff --git a/web/projects/ui/src/app/routes/portal/routes/services/routes/interface.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/routes/interface.component.ts index 628203c97..815663b27 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/routes/interface.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/routes/interface.component.ts @@ -15,6 +15,7 @@ import { TuiBadge, TuiBreadcrumbs } from '@taiga-ui/kit' import { PatchDB } from 'patch-db-client' import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component' import { getAddresses } from 'src/app/routes/portal/components/interfaces/interface.utils' +import { InterfaceStatusComponent } from 'src/app/routes/portal/components/interfaces/status.component' import { ConfigService } from 'src/app/services/config.service' import { DataModel } from 'src/app/services/patch-db/data-model' import { TitleDirective } from 'src/app/services/title.service' @@ -24,7 +25,7 @@ import { TitleDirective } from 'src/app/services/title.service' Back {{ interface()?.name }} - + @@ -32,7 +33,7 @@ import { TitleDirective } from 'src/app/services/title.service' {{ interface()?.name }} - + @if (interface(); as serviceInterface) { @@ -41,16 +42,6 @@ import { TitleDirective } from 'src/app/services/title.service' [serviceInterface]="serviceInterface" /> } - - - {{ interface()?.public ? 'Public' : 'Private' }} - - `, styles: ` :host-context(tui-root._mobile) tui-breadcrumbs { @@ -70,6 +61,7 @@ import { TitleDirective } from 'src/app/services/title.service' TuiLink, TuiBadge, NgTemplateOutlet, + InterfaceStatusComponent, ], }) export default class ServiceInterfaceRoute { diff --git a/web/projects/ui/src/app/routes/portal/routes/services/routes/service.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/routes/service.component.ts index d11819ca0..c9eceb7bc 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/routes/service.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/routes/service.component.ts @@ -19,7 +19,7 @@ import { } from 'src/app/services/patch-db/data-model' import { getInstalledPrimaryStatus } from 'src/app/services/pkg-status-rendering.service' import { ServiceActionRequestsComponent } from '../components/action-requests.component' -import { ServiceActionsComponent } from '../components/actions.component' +import { ServiceControlsComponent } from '../components/controls.component' import { ServiceDependenciesComponent } from '../components/dependencies.component' import { ServiceErrorComponent } from '../components/error.component' import { ServiceHealthChecksComponent } from '../components/health-checks.component' @@ -38,7 +38,7 @@ import { ServiceStatusComponent } from '../components/status.component'

} @if (installed() && connected()) { - + } @@ -90,7 +90,7 @@ import { ServiceStatusComponent } from '../components/status.component' CommonModule, ServiceProgressComponent, ServiceStatusComponent, - ServiceActionsComponent, + ServiceControlsComponent, ServiceInterfacesComponent, ServiceHealthChecksComponent, ServiceDependenciesComponent, diff --git a/web/projects/ui/src/app/routes/portal/routes/services/types/dependency-info.ts b/web/projects/ui/src/app/routes/portal/routes/services/types/dependency-info.ts deleted file mode 100644 index cedf01ebc..000000000 --- a/web/projects/ui/src/app/routes/portal/routes/services/types/dependency-info.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface DependencyInfo { - id: string - title: string | null - icon: string | null - version: string - errorText: string - actionText: string - action: () => any -} diff --git a/web/projects/ui/src/app/routes/portal/routes/services/types/mapped-interface.ts b/web/projects/ui/src/app/routes/portal/routes/services/types/mapped-interface.ts deleted file mode 100644 index e0aefb2a0..000000000 --- a/web/projects/ui/src/app/routes/portal/routes/services/types/mapped-interface.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { T } from '@start9labs/start-sdk' - -export type MappedInterface = T.ServiceInterface & { - public: boolean - // TODO implement addresses - addresses: any - routerLink: string -} - -export type MappedAddress = { - name: string - url: string - isDomain: boolean - isOnion: boolean - acme: string | null -} diff --git a/web/projects/ui/src/app/routes/portal/routes/sideload/package.component.ts b/web/projects/ui/src/app/routes/portal/routes/sideload/package.component.ts index f043baa18..67d45f181 100644 --- a/web/projects/ui/src/app/routes/portal/routes/sideload/package.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/sideload/package.component.ts @@ -1,11 +1,11 @@ import { CommonModule } from '@angular/common' import { Component, inject, Input } from '@angular/core' -import { Router, RouterLink } from '@angular/router' import { AboutModule, AdditionalModule, MarketplaceDependenciesComponent, MarketplacePackageHeroComponent, + MarketplacePkgBase, } from '@start9labs/marketplace' import { ErrorService, @@ -13,66 +13,39 @@ import { LoadingService, SharedPipesModule, } from '@start9labs/shared' -import { T } from '@start9labs/start-sdk' -import { TuiLet } from '@taiga-ui/cdk' -import { TuiButton } from '@taiga-ui/core' -import { TuiProgressBar } from '@taiga-ui/kit' -import { PatchDB } from 'patch-db-client' -import { combineLatest, filter, firstValueFrom, map } from 'rxjs' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ClientStorageService } from 'src/app/services/client-storage.service' +import { MarketplaceControlsComponent } from '../marketplace/components/controls.component' +import { filter, first, map } from 'rxjs' +import { PatchDB } from 'patch-db-client' import { DataModel } from 'src/app/services/patch-db/data-model' import { getManifest } from 'src/app/utils/get-package-data' -import { InstallingProgressPipe } from 'src/app/routes/portal/routes/services/pipes/install-progress.pipe' -import { SideloadService } from './sideload.service' +import { MarketplacePkgSideload } from './sideload.utils' @Component({ selector: 'sideload-package', template: `
- @if (progress$ | async; as progress) { - @for (phase of progress.phases; track $index) { -

- {{ phase.name }} - @if (phase.progress | installingProgress; as progress) { - : {{ progress }}% - } -

- - } - } @else { - -
- @if (button !== null && button !== 'Install') { - - View installed - - } - @if (button) { - - } -
-
- - - - - - - } + + + +
+
+ + @if (!(pkg.dependencyMetadata | empty)) { + + } +
+
+ +
+
`, styles: [ @@ -83,95 +56,97 @@ import { SideloadService } from './sideload.service' width: 100%; @media (min-width: 1024px) { - max-width: 80%; margin: auto; padding: 2.5rem 4rem 2rem 4rem; } } - .inner-container { + .package-details { + -moz-column-gap: 2rem; + column-gap: 2rem; + + &-main { + grid-column: span 12 / span 12; + } + + &-additional { + grid-column: span 12 / span 12; + } + + @media (min-width: 1536px) { + grid-template-columns: repeat(12, minmax(0, 1fr)); + &-main { + grid-column: span 8 / span 8; + } + &-additional { + grid-column: span 4 / span 4; + margin-top: 0px; + } + } + } + + .controls-wrapper { display: flex; justify-content: flex-start; - margin: -0.5rem 0 1.5rem -1px; + gap: 0.5rem; + height: 4.5rem; } `, ], standalone: true, imports: [ CommonModule, - RouterLink, SharedPipesModule, AboutModule, AdditionalModule, - TuiButton, - TuiLet, MarketplacePackageHeroComponent, MarketplaceDependenciesComponent, - InstallingProgressPipe, - TuiProgressBar, + MarketplaceControlsComponent, ], }) export class SideloadPackageComponent { private readonly loader = inject(LoadingService) private readonly api = inject(ApiService) private readonly errorService = inject(ErrorService) - private readonly router = inject(Router) private readonly exver = inject(Exver) - private readonly sideloadService = inject(SideloadService) + private readonly patch = inject>(PatchDB) - readonly progress$ = this.sideloadService.progress$ - readonly button$ = combineLatest([ - inject(ClientStorageService).showDevTools$, - inject>(PatchDB) - .watch$('packageData') - .pipe( - map(local => - local[this.package.id] - ? this.exver.compareExver( - getManifest(local[this.package.id]).version, - this.package.version, - ) - : null, - ), - ), - ]).pipe( - map(([devtools, version]) => { - switch (version) { - case null: - return 'Install' - case 1: - return 'Update' - case -1: - return devtools ? 'Downgrade' : '' - default: - return '' - } - }), - ) + // @Input({ required: true }) + // pkg!: MarketplacePkgSideload + // @Alex why do I need to initialize pkg below? I would prefer to do the above, but it's not working @Input({ required: true }) - package!: T.Manifest & { icon: string } + pkg: MarketplacePkgSideload = {} as MarketplacePkgSideload @Input({ required: true }) file!: File + readonly local$ = this.patch.watch$('packageData', this.pkg.id).pipe( + filter(Boolean), + map(pkg => + this.exver.getFlavor(getManifest(pkg).version) === this.pkg.flavor + ? pkg + : null, + ), + first(), + ) + + readonly flavor$ = this.local$.pipe(map(pkg => !pkg)) + + onStatic(type: 'License' | 'Instructions') { + // @TODO Matt display License or Instructions + } + async upload() { const loader = this.loader.open('Starting upload').subscribe() try { - const { upload, progress } = await this.api.sideloadPackage() - - this.sideloadService.followProgress(progress) + const { upload } = await this.api.sideloadPackage() this.api.uploadPackage(upload, this.file).catch(console.error) - await firstValueFrom(this.progress$.pipe(filter(Boolean))) } catch (e: any) { this.errorService.handleError(e) } finally { loader.unsubscribe() } } - - open(id: string) { - this.router.navigate(['/marketplace'], { queryParams: { id } }) - } } diff --git a/web/projects/ui/src/app/routes/portal/routes/sideload/sideload.component.ts b/web/projects/ui/src/app/routes/portal/routes/sideload/sideload.component.ts index b98733b7e..f4e407c91 100644 --- a/web/projects/ui/src/app/routes/portal/routes/sideload/sideload.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/sideload/sideload.component.ts @@ -1,12 +1,11 @@ import { ChangeDetectionStrategy, - ChangeDetectorRef, Component, inject, signal, } from '@angular/core' import { FormsModule } from '@angular/forms' -import { T } from '@start9labs/start-sdk' +import { MarketplacePkgBase } from '@start9labs/marketplace' import { tuiIsString } from '@taiga-ui/cdk' import { TuiButton } from '@taiga-ui/core' import { @@ -17,16 +16,16 @@ import { import { ConfigService } from 'src/app/services/config.service' import { TitleDirective } from 'src/app/services/title.service' import { SideloadPackageComponent } from './package.component' -import { parseS9pk } from './sideload.utils' +import { MarketplacePkgSideload, validateS9pk } from './sideload.utils' @Component({ template: ` Sideload - @if (file && package()) { - + @if (file && package(); as pkg) { +

@@ -69,7 +71,7 @@ import { parseS9pk } from './sideload.utils' ` label { height: 100%; - max-width: 40rem; + max-width: 42rem; margin: 0 auto; } @@ -91,11 +93,10 @@ import { parseS9pk } from './sideload.utils' ], }) export default class SideloadComponent { - private readonly cdr = inject(ChangeDetectorRef) readonly isTor = inject(ConfigService).isTor() file: File | null = null - readonly package = signal<(T.Manifest & { icon: string }) | null>(null) + readonly package = signal(null) readonly error = signal('') clear() { @@ -105,12 +106,11 @@ export default class SideloadComponent { } async onFile(file: File | null) { - const parsed = file ? await parseS9pk(file) : '' - this.file = file + + const parsed = file ? await validateS9pk(file) : '' + this.package.set(tuiIsString(parsed) ? null : parsed) this.error.set(tuiIsString(parsed) ? parsed : '') - // @TODO Alex figure out why it is needed even though we use signals - this.cdr.markForCheck() } } diff --git a/web/projects/ui/src/app/routes/portal/routes/sideload/sideload.service.ts b/web/projects/ui/src/app/routes/portal/routes/sideload/sideload.service.ts deleted file mode 100644 index 7590a0409..000000000 --- a/web/projects/ui/src/app/routes/portal/routes/sideload/sideload.service.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { inject, Injectable } from '@angular/core' -import { Router } from '@angular/router' -import { ErrorService } from '@start9labs/shared' -import { T } from '@start9labs/start-sdk' -import { - catchError, - EMPTY, - endWith, - shareReplay, - Subject, - switchMap, - tap, -} from 'rxjs' -import { ApiService } from 'src/app/services/api/embassy-api.service' - -@Injectable({ - providedIn: 'root', -}) -export class SideloadService { - private readonly api = inject(ApiService) - private readonly guid$ = new Subject() - private readonly errorService = inject(ErrorService) - private readonly router = inject(Router) - - readonly progress$ = this.guid$.pipe( - switchMap(guid => - this.api - .openWebsocket$(guid, { - closeObserver: { - next: event => { - if (event.code !== 1000) { - this.errorService.handleError(event.reason) - } - }, - }, - }) - .pipe( - tap(p => { - if (p.overall === true) { - this.router.navigate([''], { replaceUrl: true }) - } - }), - endWith(null), - ), - ), - catchError(e => { - this.errorService.handleError('Websocket connection broken. Try again.') - return EMPTY - }), - shareReplay(1), - ) - - followProgress(guid: string) { - this.guid$.next(guid) - } -} diff --git a/web/projects/ui/src/app/routes/portal/routes/sideload/sideload.utils.ts b/web/projects/ui/src/app/routes/portal/routes/sideload/sideload.utils.ts index a78bb182a..73a855f86 100644 --- a/web/projects/ui/src/app/routes/portal/routes/sideload/sideload.utils.ts +++ b/web/projects/ui/src/app/routes/portal/routes/sideload/sideload.utils.ts @@ -1,31 +1,22 @@ -import { S9pk, T } from '@start9labs/start-sdk' -import cbor from 'cbor' +import { MarketplacePkgBase } from '@start9labs/marketplace' +import { S9pk, ExtendedVersion } from '@start9labs/start-sdk' const MAGIC = new Uint8Array([59, 59]) const VERSION_1 = new Uint8Array([1]) const VERSION_2 = new Uint8Array([2]) -interface Positions { - [key: string]: [bigint, bigint] // [position, length] -} - -export async function parseS9pk( +export async function validateS9pk( file: File, -): Promise<(T.Manifest & { icon: string }) | string> { +): Promise { const magic = new Uint8Array(await blobToBuffer(file.slice(0, 2))) const version = new Uint8Array(await blobToBuffer(file.slice(2, 3))) if (compare(magic, MAGIC)) { try { if (compare(version, VERSION_1)) { - return await parseS9pkV1(file) + return 'Version 1 s9pk detected. This package format is deprecated. You can sideload a V1 s9pk via start-cli if necessary.' } else if (compare(version, VERSION_2)) { - const s9pk = await S9pk.deserialize(file, null) - - return { - ...s9pk.manifest, - icon: await s9pk.icon(), - } + return await parseS9pk(file) } else { console.error(version) @@ -43,92 +34,21 @@ export async function parseS9pk( return 'Invalid package file' } -async function parseS9pkV1(file: File) { - const positions: Positions = {} - // magic=2bytes, version=1bytes, pubkey=32bytes, signature=64bytes, toc_length=4bytes = 103byte is starting point - let start = 103 - let end = start + 1 // 104 - const tocLength = new DataView( - await blobToBuffer(file.slice(99, 103) ?? new Blob()), - ).getUint32(0, false) - await getPositions(start, end, file, positions, tocLength as any) - - const data = await blobToBuffer( - file.slice( - Number(positions['manifest'][0]), - Number(positions['manifest'][0]) + Number(positions['manifest'][1]), - ), - ) +async function parseS9pk(file: File): Promise { + const s9pk = await S9pk.deserialize(file, null) return { - ...(await cbor.decode(data, true)), - icon: await blobToDataURL( - file.slice( - Number(positions['icon'][0]), - Number(positions['icon'][0]) + Number(positions['icon'][1]), - '', - ), - ), + ...s9pk.manifest, + dependencyMetadata: await s9pk.dependencyMetadata(), + gitHash: '', + icon: await s9pk.icon(), + sourceVersion: s9pk.manifest.canMigrateFrom, + flavor: ExtendedVersion.parse(s9pk.manifest.version).flavor, + license: await s9pk.license(), + instructions: await s9pk.instructions(), } } -async function getPositions( - initialStart: number, - initialEnd: number, - file: Blob, - positions: Positions, - tocLength: number, -) { - let start = initialStart - let end = initialEnd - const titleLength = new Uint8Array( - await blobToBuffer(file.slice(start, end)), - )[0] - const tocTitle = await file.slice(end, end + titleLength).text() - start = end + titleLength - end = start + 8 - const chapterPosition = new DataView( - await blobToBuffer(file.slice(start, end)), - ).getBigUint64(0, false) - start = end - end = start + 8 - const chapterLength = new DataView( - await blobToBuffer(file.slice(start, end)), - ).getBigUint64(0, false) - - positions[tocTitle] = [chapterPosition, chapterLength] - start = end - end = start + 1 - if (end <= tocLength + (initialStart - 1)) { - await getPositions(start, end, file, positions, tocLength) - } -} - -async function readBlobAsDataURL( - f: Blob | File, -): Promise { - const reader = new FileReader() - return new Promise((resolve, reject) => { - reader.onloadend = () => { - resolve(reader.result) - } - reader.readAsDataURL(f) - reader.onerror = _ => reject(new Error('error reading blob')) - }) -} - -async function blobToDataURL(data: Blob | File): Promise { - const res = await readBlobAsDataURL(data) - if (res instanceof ArrayBuffer) { - throw new Error('readBlobAsDataURL response should not be an array buffer') - } - if (res == null) { - throw new Error('readBlobAsDataURL response should not be null') - } - if (typeof res === 'string') return res - throw new Error('no possible blob to data url resolution found') -} - async function blobToBuffer(data: Blob | File): Promise { const res = await readBlobToArrayBuffer(data) if (res instanceof String) { @@ -158,3 +78,8 @@ async function readBlobToArrayBuffer( function compare(a: Uint8Array, b: Uint8Array) { return a.length === b.length && a.every((value, index) => value === b[index]) } + +export type MarketplacePkgSideload = MarketplacePkgBase & { + license: string + instructions: string +} diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/acme/acme.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/acme/acme.component.ts index 6ef06ecc0..abf7d806e 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/acme/acme.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/acme/acme.component.ts @@ -93,7 +93,7 @@ import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec' `, styles: ` :host { - max-width: 40rem; + max-width: 36rem; } `, changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/domains.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/domains.component.ts index cf9d723a9..5224f7490 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/domains.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/domains.component.ts @@ -140,7 +140,7 @@ export default class SystemDomainsComponent { this.formDialog.open(FormComponent, options) } - // @TODO figure out how to get types here + // @TODO 041 figure out how to get types here private getNetworkStrategy(strategy: any) { return strategy.selection === 'local' ? { ipStrategy: strategy.value.ipStrategy } @@ -162,7 +162,7 @@ export default class SystemDomainsComponent { loader.unsubscribe() } } - // @TODO figure out how to get types here + // @TODO 041 figure out how to get types here private async claimDomain({ strategy }: any): Promise { const loader = this.loader.open('Saving...').subscribe() const networkStrategy = this.getNetworkStrategy(strategy) @@ -177,7 +177,7 @@ export default class SystemDomainsComponent { loader.unsubscribe() } } - // @TODO figure out how to get types here + // @TODO 041 figure out how to get types here private async save({ provider, strategy, hostname }: any): Promise { const loader = this.loader.open('Saving...').subscribe() const name = provider.selection diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/email/email.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/email/email.component.ts index 53aa66425..404f33631 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/email/email.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/email/email.component.ts @@ -97,7 +97,7 @@ import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec' `, styles: ` :host { - max-width: 40rem; + max-width: 36rem; } form header, diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/interfaces/interfaces.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/interfaces/interfaces.component.ts index 15e411cff..3bc241cdf 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/interfaces/interfaces.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/interfaces/interfaces.component.ts @@ -8,6 +8,7 @@ import { PatchDB } from 'patch-db-client' import { map } from 'rxjs' import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component' import { getAddresses } from 'src/app/routes/portal/components/interfaces/interface.utils' +import { InterfaceStatusComponent } from 'src/app/routes/portal/components/interfaces/status.component' import { ConfigService } from 'src/app/services/config.service' import { DataModel } from 'src/app/services/patch-db/data-model' import { TitleDirective } from 'src/app/services/title.service' @@ -34,10 +35,14 @@ const iface: T.ServiceInterface = { Back StartOS UI +
-

{{ iface.name }}

+

+ {{ iface.name }} + +

{{ iface.description }}

@@ -54,6 +59,7 @@ const iface: T.ServiceInterface = { TitleDirective, TuiHeader, TuiTitle, + InterfaceStatusComponent, ], }) export default class StartOsUiComponent { diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/proxies/proxies.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/proxies/proxies.component.ts index 3234beed1..a8a4a352c 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/proxies/proxies.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/proxies/proxies.component.ts @@ -62,7 +62,7 @@ export default class SystemProxiesComponent { this.formDialog.open(FormComponent, options) } - // @TODO fix type to be WireguardSpec + // @TODO 041 fix type to be WireguardSpec private async save({ name, config }: any): Promise { const loader = this.loader.open('Saving...').subscribe() diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/sessions/platform-info.pipe.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/sessions/platform-info.pipe.ts index 0398dbbf1..c3019cc04 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/sessions/platform-info.pipe.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/sessions/platform-info.pipe.ts @@ -1,38 +1,45 @@ import { Pipe, PipeTransform } from '@angular/core' -import { PlatformType } from 'src/app/services/api/api.types' @Pipe({ name: 'platformInfo', standalone: true, }) export class PlatformInfoPipe implements PipeTransform { - transform(platforms: readonly PlatformType[]): { + transform(userAgent: string | null): { name: string icon: string } { - const info = { - name: '', - icon: '@tui.smartphone', + if (!userAgent) { + return { + name: 'CLI', + icon: '@tui.terminal', + } } - if (platforms.includes('cli')) { - info.name = 'CLI' - info.icon = '@tui.terminal' - } else if (platforms.includes('desktop')) { - info.name = 'Desktop/Laptop' - info.icon = '@tui.monitor' - } else if (platforms.includes('android')) { - info.name = 'Android Device' - } else if (platforms.includes('iphone')) { - info.name = 'iPhone' - } else if (platforms.includes('ipad')) { - info.name = 'iPad' - } else if (platforms.includes('ios')) { - info.name = 'iOS Device' - } else { - info.name = 'Unknown Device' + if (/Android/i.test(userAgent)) { + return { + name: 'Android Device', + icon: '@tui.smartphone', + } } - return info + if (/iPhone/i.test(userAgent)) { + return { + name: 'iPhone', + icon: '@tui.smartphone', + } + } + + if (/iPad/i.test(userAgent)) { + return { + name: 'iPad', + icon: '@tui.smartphone', + } + } + + return { + name: 'Desktop/Laptop', + icon: '@tui.monitor', + } } } diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/sessions/sessions.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/sessions/sessions.component.ts index 8b142d316..89234a831 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/sessions/sessions.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/sessions/sessions.component.ts @@ -1,5 +1,4 @@ import { RouterLink } from '@angular/router' -import { TuiTable } from '@taiga-ui/addon-table' import { TuiLet } from '@taiga-ui/cdk' import { TuiButton, TuiTitle } from '@taiga-ui/core' import { CommonModule } from '@angular/common' diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/sessions/table.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/sessions/table.component.ts index 2fd62bbdc..5b27db20b 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/sessions/table.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/sessions/table.component.ts @@ -33,10 +33,10 @@ import { PlatformInfoPipe } from './platform-info.pipe' {{ session.userAgent }} - @if (session.metadata.platforms | platformInfo; as info) { + @if (session.userAgent | platformInfo; as platform) { - - {{ info.name }} + + {{ platform.name }} } {{ session.lastActive | date: 'medium' }} diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/wifi/wifi.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/wifi/wifi.component.ts index bd1b2861e..69858b535 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/wifi/wifi.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/wifi/wifi.component.ts @@ -103,7 +103,7 @@ import { wifiSpec } from './wifi.const' `, styles: ` :host { - max-width: 40rem; + max-width: 36rem; } `, changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/web/projects/ui/src/app/services/api/api.fixures.ts b/web/projects/ui/src/app/services/api/api.fixures.ts index ca4a54073..a9cb0b70e 100644 --- a/web/projects/ui/src/app/services/api/api.fixures.ts +++ b/web/projects/ui/src/app/services/api/api.fixures.ts @@ -935,26 +935,17 @@ export namespace Mock { loggedIn: '2021-07-14T20:49:17.774Z', lastActive: '2021-07-14T20:49:17.774Z', userAgent: 'AppleWebKit/{WebKit Rev} (KHTML, like Gecko)', - metadata: { - platforms: ['iphone', 'mobileweb', 'mobile', 'ios'], - }, }, klndsfjhbwsajkdnaksj: { loggedIn: '2021-07-14T20:49:17.774Z', lastActive: '2019-07-14T20:49:17.774Z', userAgent: 'AppleWebKit/{WebKit Rev} (KHTML, like Gecko)', - metadata: { - platforms: ['cli'], - }, }, b7b1a9cef4284f00af9e9dda6e676177: { loggedIn: '2021-07-14T20:49:17.774Z', lastActive: '2021-06-14T20:49:17.774Z', userAgent: 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0', - metadata: { - platforms: ['desktop'], - }, }, }, } @@ -1500,7 +1491,6 @@ export namespace Mock { }, { spec: ISB.InputSpec.of({ - /* TODO: Convert range for this value ([0, 2])*/ union: ISB.Value.union( { name: 'Preference', @@ -1560,19 +1550,18 @@ export namespace Mock { }, disabled: ['option2'], })), - 'favorite-number': - /* TODO: Convert range for this value ((-100,100])*/ ISB.Value.number( - { - name: 'Favorite Number', - description: 'Your favorite number of all time', - warning: - 'Once you set this number, it can never be changed without severe consequences.', - required: false, - default: 7, - integer: false, - units: 'BTC', - }, - ), + 'favorite-number': ISB.Value.number({ + name: 'Favorite Number', + description: 'Your favorite number of all time', + warning: + 'Once you set this number, it can never be changed without severe consequences.', + required: false, + default: 7, + integer: false, + units: 'BTC', + min: -100, + max: 100, + }), rpcsettings: ISB.Value.object( { name: 'RPC Settings', @@ -1906,7 +1895,7 @@ export namespace Mock { name: 'View Properties', description: 'view important information about Bitcoin', warning: null, - visibility: 'enabled', + visibility: 'hidden', allowedStatuses: 'any', hasInput: false, group: null, diff --git a/web/projects/ui/src/app/services/api/api.types.ts b/web/projects/ui/src/app/services/api/api.types.ts index d039461f0..50ef1826a 100644 --- a/web/projects/ui/src/app/services/api/api.types.ts +++ b/web/projects/ui/src/app/services/api/api.types.ts @@ -31,7 +31,6 @@ export namespace RR { export type LoginReq = { password: string - metadata: SessionMetadata ephemeral?: boolean } // auth.login - unauthed export type loginRes = null @@ -421,30 +420,8 @@ export type Session = { loggedIn: string lastActive: string userAgent: string - metadata: SessionMetadata } -export type SessionMetadata = { - platforms: PlatformType[] -} - -export type PlatformType = - | 'cli' - | 'ios' - | 'ipad' - | 'iphone' - | 'android' - | 'phablet' - | 'tablet' - | 'cordova' - | 'capacitor' - | 'electron' - | 'pwa' - | 'mobile' - | 'mobileweb' - | 'desktop' - | 'hybrid' - export type BackupTarget = DiskBackupTarget | CifsBackupTarget export interface DiskBackupTarget { @@ -604,7 +581,7 @@ export type DependencyErrorTransitive = { type: 'transitive' } -// **** @TODO 041 **** +// @TODO 041 // export namespace RR041 { // // ** domains ** diff --git a/web/projects/ui/src/app/services/api/mock-patch.ts b/web/projects/ui/src/app/services/api/mock-patch.ts index 6b17e6fae..005eb62b7 100644 --- a/web/projects/ui/src/app/services/api/mock-patch.ts +++ b/web/projects/ui/src/app/services/api/mock-patch.ts @@ -252,7 +252,7 @@ export const mockPatchData: DataModel = { name: 'View Properties', description: 'view important information about Bitcoin', warning: null, - visibility: 'enabled', + visibility: 'hidden', allowedStatuses: 'any', hasInput: false, group: null, diff --git a/web/projects/ui/src/app/services/form.service.ts b/web/projects/ui/src/app/services/form.service.ts index 32f78e072..e524fc9c4 100644 --- a/web/projects/ui/src/app/services/form.service.ts +++ b/web/projects/ui/src/app/services/form.service.ts @@ -357,11 +357,11 @@ export function listUnique(spec: IST.ValueSpecList): ValidatorFn { const objSpec = spec.spec let display1: string let display2: string - let uniqueMessage = isObject(objSpec) + let uniqueMessage = isListObject(objSpec) ? uniqueByMessageWrapper(objSpec.uniqueBy, objSpec) : '' - if (isObject(objSpec) && objSpec.displayAs) { + if (isListObject(objSpec) && objSpec.displayAs) { display1 = `"${(Mustache as any).render( objSpec.displayAs, list[idx], @@ -390,7 +390,6 @@ function listItemEquals( val1: any, val2: any, ): boolean { - // TODO: fix types switch (spec.spec.type) { case 'text': return val1 == val2 @@ -402,45 +401,6 @@ function listItemEquals( } } -function itemEquals(spec: IST.ValueSpec, val1: any, val2: any): boolean { - switch (spec.type) { - case 'text': - case 'textarea': - case 'number': - case 'toggle': - case 'select': - return val1 == val2 - case 'object': - // TODO: 'unique-by' does not exist on ValueSpecObject, fix types - return objEquals( - (spec as any)['unique-by'], - spec as IST.ValueSpecObject, - val1, - val2, - ) - case 'union': - // TODO: 'unique-by' does not exist onIST.ValueSpecUnion, fix types - return unionEquals( - (spec as any)['unique-by'], - spec as IST.ValueSpecUnion, - val1, - val2, - ) - case 'list': - if (val1.length !== val2.length) { - return false - } - for (let idx = 0; idx < val1.length; idx++) { - if (listItemEquals(spec, val1[idx], val2[idx])) { - return false - } - } - return true - default: - return false - } -} - function listObjEquals( uniqueBy: IST.UniqueBy, spec: IST.ListValueSpecObject, @@ -450,17 +410,17 @@ function listObjEquals( if (!uniqueBy) { return false } else if (typeof uniqueBy === 'string') { - return itemEquals(spec.spec[uniqueBy], val1[uniqueBy], val2[uniqueBy]) + return uniqueByEquals(spec.spec[uniqueBy], val1[uniqueBy], val2[uniqueBy]) } else if ('any' in uniqueBy) { - for (let subSpec of uniqueBy.any) { - if (listObjEquals(subSpec, spec, val1, val2)) { + for (let unique of uniqueBy.any) { + if (listObjEquals(unique, spec, val1, val2)) { return true } } return false } else if ('all' in uniqueBy) { - for (let subSpec of uniqueBy.all) { - if (!listObjEquals(subSpec, spec, val1, val2)) { + for (let unique of uniqueBy.all) { + if (!listObjEquals(unique, spec, val1, val2)) { return false } } @@ -469,66 +429,29 @@ function listObjEquals( return false } -function objEquals( - uniqueBy: IST.UniqueBy, - spec: IST.ValueSpecObject, - val1: any, - val2: any, -): boolean { - if (!uniqueBy) { - return false - } else if (typeof uniqueBy === 'string') { - // TODO: fix types - return itemEquals((spec as any)[uniqueBy], val1[uniqueBy], val2[uniqueBy]) - } else if ('any' in uniqueBy) { - for (let subSpec of uniqueBy.any) { - if (objEquals(subSpec, spec, val1, val2)) { - return true - } - } - return false - } else if ('all' in uniqueBy) { - for (let subSpec of uniqueBy.all) { - if (!objEquals(subSpec, spec, val1, val2)) { +function uniqueByEquals(spec: IST.ValueSpec, val1: any, val2: any): boolean { + switch (spec.type) { + case 'text': + case 'textarea': + case 'number': + case 'toggle': + case 'select': + case 'color': + case 'datetime': + return val1 == val2 + case 'list': + if (val1.length !== val2.length) { return false } - } - return true - } - return false -} - -function unionEquals( - uniqueBy: IST.UniqueBy, - spec: IST.ValueSpecUnion, - val1: any, - val2: any, -): boolean { - const variantSpec = spec.variants[val1.selection].spec - if (!uniqueBy) { - return false - } else if (typeof uniqueBy === 'string') { - if (uniqueBy === 'selection') { - return val1.selection === val2.selection - } else { - return itemEquals(variantSpec[uniqueBy], val1[uniqueBy], val2[uniqueBy]) - } - } else if ('any' in uniqueBy) { - for (let subSpec of uniqueBy.any) { - if (unionEquals(subSpec, spec, val1, val2)) { - return true + for (let idx = 0; idx < val1.length; idx++) { + if (listItemEquals(spec, val1[idx], val2[idx])) { + return false + } } - } - return false - } else if ('all' in uniqueBy) { - for (let subSpec of uniqueBy.all) { - if (!unionEquals(subSpec, spec, val1, val2)) { - return false - } - } - return true + return true + default: + return false } - return false } function uniqueByMessageWrapper( @@ -573,7 +496,7 @@ function uniqueByMessage( : '(' + ret + ')' } -function isObject( +function isListObject( spec: IST.ListValueSpecOf, ): spec is IST.ListValueSpecObject { // only lists of objects have uniqueBy diff --git a/web/projects/ui/src/app/services/marketplace.service.ts b/web/projects/ui/src/app/services/marketplace.service.ts index 7c82ed624..b6697b7c5 100644 --- a/web/projects/ui/src/app/services/marketplace.service.ts +++ b/web/projects/ui/src/app/services/marketplace.service.ts @@ -48,7 +48,6 @@ export class MarketplaceService { this.registryUrl$.pipe( switchMap(url => this.fetchRegistry$(url)), filter(Boolean), - // @TODO is updateStoreName needed? map(registry => { registry.info.categories = { all: { @@ -217,7 +216,7 @@ export class MarketplaceService { map(packages => { return Object.entries(packages).flatMap(([id, pkgInfo]) => Object.keys(pkgInfo.best).map(version => - this.convertToMarketplacePkg( + this.convertRegistryPkgToMarketplacePkg( id, version, this.exver.getFlavor(version), @@ -239,12 +238,12 @@ export class MarketplaceService { this.api.getRegistryPackage(url, id, version ? `=${version}` : null), ).pipe( map(pkgInfo => - this.convertToMarketplacePkg(id, version, flavor, pkgInfo), + this.convertRegistryPkgToMarketplacePkg(id, version, flavor, pkgInfo), ), ) } - private convertToMarketplacePkg( + private convertRegistryPkgToMarketplacePkg( id: string, version: string | null | undefined, flavor: string | null, diff --git a/web/projects/ui/src/styles.scss b/web/projects/ui/src/styles.scss index 66eeb1876..3ab4c9db8 100644 --- a/web/projects/ui/src/styles.scss +++ b/web/projects/ui/src/styles.scss @@ -40,7 +40,7 @@ hr { top left, top right; - // TODO: Theme + // @TODO Theme background-color: color-mix( in hsl, var(--tui-background-base) 90%, diff --git a/web/tsconfig.json b/web/tsconfig.json index afc63ec00..5f10deca8 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -22,7 +22,6 @@ "allowSyntheticDefaultImports": true, "paths": { /* These paths are relative to each app base folder */ - /* @TODO Alex the marketplace path is different on 0351. verify */ "@start9labs/marketplace": ["../marketplace/index"], "@start9labs/shared": ["../shared/src/public-api"], "path": ["../../node_modules/path-browserify"]