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 <me@drbonez.dev>
Co-authored-by: waterplea <alexander@inkin.ru>
Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com>
This commit is contained in:
Matt Hill
2025-04-10 13:51:05 -06:00
committed by GitHub
parent ab4336cfd7
commit fc2be42418
88 changed files with 773 additions and 965 deletions

View File

@@ -197,9 +197,6 @@ pub struct LoginParams {
user_agent: Option<String>, user_agent: Option<String>,
#[serde(default)] #[serde(default)]
ephemeral: bool, ephemeral: bool,
#[serde(default)]
#[ts(type = "any")]
metadata: Value,
} }
#[instrument(skip_all)] #[instrument(skip_all)]
@@ -209,7 +206,6 @@ pub async fn login_impl(
password, password,
user_agent, user_agent,
ephemeral, ephemeral,
metadata,
}: LoginParams, }: LoginParams,
) -> Result<LoginRes, Error> { ) -> Result<LoginRes, Error> {
let password = password.unwrap_or_default().decrypt(&ctx)?; let password = password.unwrap_or_default().decrypt(&ctx)?;
@@ -224,7 +220,6 @@ pub async fn login_impl(
logged_in: Utc::now(), logged_in: Utc::now(),
last_active: Utc::now(), last_active: Utc::now(),
user_agent, user_agent,
metadata,
}, },
) )
}); });
@@ -240,7 +235,6 @@ pub async fn login_impl(
logged_in: Utc::now(), logged_in: Utc::now(),
last_active: Utc::now(), last_active: Utc::now(),
user_agent, user_agent,
metadata,
}, },
)?; )?;
@@ -277,10 +271,7 @@ pub struct Session {
pub logged_in: DateTime<Utc>, pub logged_in: DateTime<Utc>,
#[ts(type = "string")] #[ts(type = "string")]
pub last_active: DateTime<Utc>, pub last_active: DateTime<Utc>,
#[ts(skip)]
pub user_agent: Option<String>, pub user_agent: Option<String>,
#[ts(type = "any")]
pub metadata: Value,
} }
#[derive(Deserialize, Serialize, TS)] #[derive(Deserialize, Serialize, TS)]
@@ -327,7 +318,6 @@ fn display_sessions(params: WithIoFormat<ListParams>, arg: SessionList) {
"LOGGED IN", "LOGGED IN",
"LAST ACTIVE", "LAST ACTIVE",
"USER AGENT", "USER AGENT",
"METADATA",
]); ]);
for (id, session) in arg.sessions.0 { for (id, session) in arg.sessions.0 {
let mut row = row![ let mut row = row![
@@ -335,7 +325,6 @@ fn display_sessions(params: WithIoFormat<ListParams>, arg: SessionList) {
&format!("{}", session.logged_in), &format!("{}", session.logged_in),
&format!("{}", session.last_active), &format!("{}", session.last_active),
session.user_agent.as_deref().unwrap_or("N/A"), session.user_agent.as_deref().unwrap_or("N/A"),
&format!("{}", session.metadata),
]; ];
if Some(id) == arg.current { if Some(id) == arg.current {
row.iter_mut() row.iter_mut()

View File

@@ -186,6 +186,7 @@ pub async fn remove_before(
Ok(()) Ok(())
}) })
.await .await
.result
} }
pub async fn mark_seen( pub async fn mark_seen(
@@ -213,6 +214,7 @@ pub async fn mark_seen(
Ok(()) Ok(())
}) })
.await .await
.result
} }
pub async fn mark_seen_before( pub async fn mark_seen_before(
@@ -240,6 +242,7 @@ pub async fn mark_seen_before(
Ok(()) Ok(())
}) })
.await .await
.result
} }
pub async fn mark_unseen( pub async fn mark_unseen(

View File

@@ -72,7 +72,7 @@ pub struct PackageVersionInfo {
pub icon: DataUrl<'static>, pub icon: DataUrl<'static>,
pub description: Description, pub description: Description,
pub release_notes: String, pub release_notes: String,
pub git_hash: GitHash, pub git_hash: Option<GitHash>,
#[ts(type = "string")] #[ts(type = "string")]
pub license: InternedString, pub license: InternedString,
#[ts(type = "string")] #[ts(type = "string")]
@@ -115,7 +115,7 @@ impl PackageVersionInfo {
icon: s9pk.icon_data_url().await?, icon: s9pk.icon_data_url().await?,
description: manifest.description.clone(), description: manifest.description.clone(),
release_notes: manifest.release_notes.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(), license: manifest.license.clone(),
wrapper_repo: manifest.wrapper_repo.clone(), wrapper_repo: manifest.wrapper_repo.clone(),
upstream_repo: manifest.upstream_repo.clone(), upstream_repo: manifest.upstream_repo.clone(),
@@ -153,7 +153,7 @@ impl PackageVersionInfo {
br -> "DESCRIPTION", br -> "DESCRIPTION",
&textwrap::wrap(&self.description.long, 80).join("\n") &textwrap::wrap(&self.description.long, 80).join("\n")
]); ]);
table.add_row(row![br -> "GIT HASH", AsRef::<str>::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 -> "LICENSE", &self.license]);
table.add_row(row![br -> "PACKAGE REPO", &self.wrapper_repo.to_string()]); table.add_row(row![br -> "PACKAGE REPO", &self.wrapper_repo.to_string()]);
table.add_row(row![br -> "SERVICE REPO", &self.upstream_repo.to_string()]); table.add_row(row![br -> "SERVICE REPO", &self.upstream_repo.to_string()]);

View File

@@ -1,3 +1,4 @@
use std::ops::Deref;
use std::path::Path; use std::path::Path;
use tokio::process::Command; use tokio::process::Command;
@@ -66,6 +67,13 @@ impl AsRef<str> for GitHash {
} }
} }
impl Deref for GitHash {
type Target = str;
fn deref(&self) -> &Self::Target {
self.as_ref()
}
}
// #[tokio::test] // #[tokio::test]
// async fn test_githash_for_current() { // async fn test_githash_for_current() {
// let answer: GitHash = GitHash::from_path(std::env::current_dir().unwrap()) // let answer: GitHash = GitHash::from_path(std::env::current_dir().unwrap())

View File

@@ -26,7 +26,6 @@ use crate::s9pk::merkle_archive::source::{
into_dyn_read, ArchiveSource, DynFileSource, DynRead, FileSource, TmpSource, into_dyn_read, ArchiveSource, DynFileSource, DynRead, FileSource, TmpSource,
}; };
use crate::s9pk::merkle_archive::{Entry, MerkleArchive}; use crate::s9pk::merkle_archive::{Entry, MerkleArchive};
use crate::s9pk::v2::recipe::DirRecipe;
use crate::s9pk::v2::SIG_CONTEXT; use crate::s9pk::v2::SIG_CONTEXT;
use crate::s9pk::S9pk; use crate::s9pk::S9pk;
use crate::util::io::{create_file, open_file, TmpDir}; 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); let dep_path = Path::new("dependencies").join(id);
to_insert.push(( to_insert.push((
dep_path.join("metadata.json"), dep_path.join("metadata.json"),
Entry::file(PackSource::Buffered( Entry::file(TmpSource::new(
IoFormat::Json tmp_dir.clone(),
.to_vec(&DependencyMetadata { PackSource::Buffered(
title: s9pk.as_manifest().title.clone(), IoFormat::Json
})? .to_vec(&DependencyMetadata {
.into(), title: s9pk.as_manifest().title.clone(),
})?
.into(),
),
)), )),
)); ));
let icon = s9pk.icon().await?; let icon = s9pk.icon().await?;
to_insert.push(( to_insert.push((
dep_path.join(&*icon.0), dep_path.join(&*icon.0),
Entry::file(PackSource::Buffered( Entry::file(TmpSource::new(
icon.1.expect_file()?.to_vec(icon.1.hash()).await?.into(), tmp_dir.clone(),
PackSource::Buffered(icon.1.expect_file()?.to_vec(icon.1.hash()).await?.into()),
)), )),
)); ));
} else { } else {
warn!("no s9pk specified for {id}, leaving metadata empty"); 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)?; s9pk.validate_and_filter(None)?;

View File

@@ -5,7 +5,10 @@ use imbl_value::json;
use models::{ActionId, PackageId, ProcedureName, ReplayId}; use models::{ActionId, PackageId, ProcedureName, ReplayId};
use crate::action::{ActionInput, ActionResult}; 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::prelude::*;
use crate::rpc_continuations::Guid; use crate::rpc_continuations::Guid;
use crate::service::{Service, ServiceActor}; use crate::service::{Service, ServiceActor};
@@ -123,12 +126,44 @@ impl Handler<RunAction> for ServiceActor {
&mut self, &mut self,
id: Guid, id: Guid,
RunAction { RunAction {
id: action_id, id: ref action_id,
input, input,
}: RunAction, }: RunAction,
_: &BackgroundJobQueue, _: &BackgroundJobQueue,
) -> Self::Response { ) -> Self::Response {
let container = &self.0.persistent_container; 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 let result = container
.execute::<Option<ActionResult>>( .execute::<Option<ActionResult>>(
id, id,
@@ -140,7 +175,6 @@ impl Handler<RunAction> for ServiceActor {
) )
.await .await
.with_kind(ErrorKind::Action)?; .with_kind(ErrorKind::Action)?;
let package_id = &self.0.id;
self.0 self.0
.ctx .ctx
.db .db
@@ -150,7 +184,7 @@ impl Handler<RunAction> for ServiceActor {
Ok(update_requested_actions( Ok(update_requested_actions(
requested_actions, requested_actions,
package_id, package_id,
&action_id, action_id,
&input, &input,
true, true,
)) ))

View File

@@ -66,7 +66,9 @@ export async function checkDependencies<
return dep.requirement.kind !== "running" || dep.result.isRunning return dep.requirement.kind !== "running" || dep.result.isRunning
} }
const actionsSatisfied = (packageId: DependencyId) => 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 = ( const healthCheckSatisfied = (
packageId: DependencyId, packageId: DependencyId,
healthCheckId?: HealthCheckId, healthCheckId?: HealthCheckId,
@@ -129,7 +131,9 @@ export async function checkDependencies<
} }
const throwIfActionsNotSatisfied = (packageId: DependencyId) => { const throwIfActionsNotSatisfied = (packageId: DependencyId) => {
const dep = find(packageId) 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) { if (reqs.length) {
throw new Error( throw new Error(
`The following action requests have not been fulfilled: ${reqs.join(", ")}`, `The following action requests have not been fulfilled: ${reqs.join(", ")}`,

View File

@@ -1,8 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // 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" import type { PasswordType } from "./PasswordType"
export type LoginParams = { export type LoginParams = { password: PasswordType | null; ephemeral: boolean }
password: PasswordType | null
ephemeral: boolean
metadata: any
}

View File

@@ -14,7 +14,7 @@ export type PackageVersionInfo = {
icon: DataUrl icon: DataUrl
description: Description description: Description
releaseNotes: string releaseNotes: string
gitHash: GitHash gitHash: GitHash | null
license: string license: string
wrapperRepo: string wrapperRepo: string
upstreamRepo: string upstreamRepo: string

View File

@@ -1,3 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // 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
}

View File

@@ -1,5 +1,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Sessions = { export type Sessions = {
[key: string]: { loggedIn: string; lastActive: string; metadata: any } [key: string]: {
loggedIn: string
lastActive: string
userAgent: string | null
}
} }

View File

@@ -1,6 +1,14 @@
import { DataUrl, Manifest, MerkleArchiveCommitment } from "../osBindings" import {
DataUrl,
DependencyMetadata,
Manifest,
MerkleArchiveCommitment,
PackageId,
} from "../osBindings"
import { ArrayBufferReader, MerkleArchive } from "./merkleArchive" import { ArrayBufferReader, MerkleArchive } from "./merkleArchive"
import mime from "mime-types" import mime from "mime-types"
import { DirectoryContents } from "./merkleArchive/directoryContents"
import { FileContents } from "./merkleArchive/fileContents"
const magicAndVersion = new Uint8Array([59, 59, 2]) const magicAndVersion = new Uint8Array([59, 59, 2])
@@ -65,4 +73,63 @@ export class S9pk {
).toString("base64") ).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<Record<PackageId, DependencyMetadata>> {
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<string> {
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<string> {
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())
}
} }

View File

@@ -30,7 +30,12 @@ import { HealthCheck } from "./health/HealthCheck"
import { checkPortListening } from "./health/checkFns/checkPortListening" import { checkPortListening } from "./health/checkFns/checkPortListening"
import { checkWebUrl, runHealthScript } from "./health/checkFns" import { checkWebUrl, runHealthScript } from "./health/checkFns"
import { List } from "../../base/lib/actions/input/builder/list" 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 { SetupBackupsParams, setupBackups } from "./backup/setupBackups"
import { UninstallFn, setupUninstall } from "./inits/setupUninstall" import { UninstallFn, setupUninstall } from "./inits/setupUninstall"
import { setupMain } from "./mainFn" import { setupMain } from "./mainFn"
@@ -571,12 +576,24 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
setupDependencies: setupDependencies<Manifest>, setupDependencies: setupDependencies<Manifest>,
setupInit: setupInit<Manifest, Store>, setupInit: setupInit<Manifest, Store>,
/** /**
* @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<Manifest, Store>) => PreInstall.of(fn),
/**
* @description Use this function to execute arbitrary logic *once*, on initial install *after* interfaces, actions, and dependencies are updated.
* @example * @example
* In the this example, we bootstrap our Store with a random, 16-char admin password. * 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( await sdk.store.setOwn(
effects, effects,
sdk.StorePath.adminPassword, sdk.StorePath.adminPassword,
@@ -588,10 +605,7 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
}) })
* ``` * ```
*/ */
setupInstall: ( setupPostInstall: (fn: InstallFn<Manifest, Store>) => PostInstall.of(fn),
fn: InstallFn<Manifest, Store>,
preFn?: InstallFn<Manifest, Store>,
) => Install.of(fn, preFn),
/** /**
* @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. * @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.
* *

View File

@@ -5,12 +5,13 @@ import { ExposedStorePaths } from "../../../base/lib/types"
import * as T from "../../../base/lib/types" import * as T from "../../../base/lib/types"
import { StorePath } from "../util" import { StorePath } from "../util"
import { VersionGraph } from "../version/VersionGraph" import { VersionGraph } from "../version/VersionGraph"
import { Install } from "./setupInstall" import { PostInstall, PreInstall } from "./setupInstall"
import { Uninstall } from "./setupUninstall" import { Uninstall } from "./setupUninstall"
export function setupInit<Manifest extends T.SDKManifest, Store>( export function setupInit<Manifest extends T.SDKManifest, Store>(
versions: VersionGraph<string>, versions: VersionGraph<string>,
install: Install<Manifest, Store>, preInstall: PreInstall<Manifest, Store>,
postInstall: PostInstall<Manifest, Store>,
uninstall: Uninstall<Manifest, Store>, uninstall: Uninstall<Manifest, Store>,
setServiceInterfaces: UpdateServiceInterfaces<any>, setServiceInterfaces: UpdateServiceInterfaces<any>,
setDependencies: (options: { setDependencies: (options: {
@@ -34,7 +35,7 @@ export function setupInit<Manifest extends T.SDKManifest, Store>(
to: versions.currentVersion(), to: versions.currentVersion(),
}) })
} else { } else {
await install.install(opts) await postInstall.postInstall(opts)
await opts.effects.setDataVersion({ await opts.effects.setDataVersion({
version: versions.current.options.version, version: versions.current.options.version,
}) })
@@ -61,7 +62,7 @@ export function setupInit<Manifest extends T.SDKManifest, Store>(
path: "" as StorePath, path: "" as StorePath,
value: initStore, value: initStore,
}) })
await install.preInstall(opts) await preInstall.preInstall(opts)
} }
await setServiceInterfaces({ await setServiceInterfaces({
...opts, ...opts,

View File

@@ -4,34 +4,57 @@ export type InstallFn<Manifest extends T.SDKManifest, Store> = (opts: {
effects: T.Effects effects: T.Effects
}) => Promise<null | void | undefined> }) => Promise<null | void | undefined>
export class Install<Manifest extends T.SDKManifest, Store> { export class Install<Manifest extends T.SDKManifest, Store> {
private constructor( protected constructor(readonly fn: InstallFn<Manifest, Store>) {}
readonly fn: InstallFn<Manifest, Store>, }
readonly preFn?: InstallFn<Manifest, Store>,
) {} export class PreInstall<Manifest extends T.SDKManifest, Store> extends Install<
Manifest,
Store
> {
private constructor(fn: InstallFn<Manifest, Store>) {
super(fn)
}
static of<Manifest extends T.SDKManifest, Store>( static of<Manifest extends T.SDKManifest, Store>(
fn: InstallFn<Manifest, Store>, fn: InstallFn<Manifest, Store>,
preFn?: InstallFn<Manifest, Store>,
) { ) {
return new Install(fn, preFn) return new PreInstall(fn)
} }
async install({ effects }: Parameters<T.ExpectedExports.packageInit>[0]) { async preInstall({ effects }: Parameters<T.ExpectedExports.packageInit>[0]) {
await this.fn({ await this.fn({
effects, effects,
}) })
} }
}
async preInstall({ effects }: Parameters<T.ExpectedExports.packageInit>[0]) { export function setupPreInstall<Manifest extends T.SDKManifest, Store>(
this.preFn && fn: InstallFn<Manifest, Store>,
(await this.preFn({ ) {
effects, return PreInstall.of(fn)
})) }
export class PostInstall<Manifest extends T.SDKManifest, Store> extends Install<
Manifest,
Store
> {
private constructor(fn: InstallFn<Manifest, Store>) {
super(fn)
}
static of<Manifest extends T.SDKManifest, Store>(
fn: InstallFn<Manifest, Store>,
) {
return new PostInstall(fn)
}
async postInstall({ effects }: Parameters<T.ExpectedExports.packageInit>[0]) {
await this.fn({
effects,
})
} }
} }
export function setupInstall<Manifest extends T.SDKManifest, Store>( export function setupPostInstall<Manifest extends T.SDKManifest, Store>(
fn: InstallFn<Manifest, Store>, fn: InstallFn<Manifest, Store>,
preFn?: InstallFn<Manifest, Store>,
) { ) {
return Install.of(fn, preFn) return PostInstall.of(fn)
} }

View File

@@ -1,12 +1,12 @@
{ {
"name": "@start9labs/start-sdk", "name": "@start9labs/start-sdk",
"version": "0.3.6-beta.20", "version": "0.4.0-beta.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@start9labs/start-sdk", "name": "@start9labs/start-sdk",
"version": "0.3.6-beta.20", "version": "0.4.0-beta.2",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@iarna/toml": "^2.2.5", "@iarna/toml": "^2.2.5",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@start9labs/start-sdk", "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", "description": "Software development kit to facilitate packaging services for StartOS",
"main": "./package/lib/index.js", "main": "./package/lib/index.js",
"types": "./package/lib/index.d.ts", "types": "./package/lib/index.d.ts",

234
web/package-lock.json generated
View File

@@ -25,18 +25,18 @@
"@noble/hashes": "^1.4.0", "@noble/hashes": "^1.4.0",
"@start9labs/argon2": "^0.2.2", "@start9labs/argon2": "^0.2.2",
"@start9labs/start-sdk": "file:../sdk/baseDist", "@start9labs/start-sdk": "file:../sdk/baseDist",
"@taiga-ui/addon-charts": "4.30.0", "@taiga-ui/addon-charts": "4.32.0",
"@taiga-ui/addon-commerce": "4.30.0", "@taiga-ui/addon-commerce": "4.32.0",
"@taiga-ui/addon-mobile": "4.30.0", "@taiga-ui/addon-mobile": "4.32.0",
"@taiga-ui/addon-table": "4.30.0", "@taiga-ui/addon-table": "4.32.0",
"@taiga-ui/cdk": "4.30.0", "@taiga-ui/cdk": "4.32.0",
"@taiga-ui/core": "4.30.0", "@taiga-ui/core": "4.32.0",
"@taiga-ui/event-plugins": "4.5.0", "@taiga-ui/event-plugins": "4.5.1",
"@taiga-ui/experimental": "4.30.0", "@taiga-ui/experimental": "4.32.0",
"@taiga-ui/icons": "4.30.0", "@taiga-ui/icons": "4.32.0",
"@taiga-ui/kit": "4.30.0", "@taiga-ui/kit": "4.32.0",
"@taiga-ui/layout": "4.30.0", "@taiga-ui/layout": "4.32.0",
"@taiga-ui/legacy": "4.30.0", "@taiga-ui/legacy": "4.32.0",
"@taiga-ui/polymorpheus": "4.9.0", "@taiga-ui/polymorpheus": "4.9.0",
"@tinkoff/ng-dompurify": "4.0.0", "@tinkoff/ng-dompurify": "4.0.0",
"ansi-to-html": "^0.7.2", "ansi-to-html": "^0.7.2",
@@ -3480,9 +3480,9 @@
} }
}, },
"node_modules/@ng-web-apis/common": { "node_modules/@ng-web-apis/common": {
"version": "4.11.1", "version": "4.12.0",
"resolved": "https://registry.npmjs.org/@ng-web-apis/common/-/common-4.11.1.tgz", "resolved": "https://registry.npmjs.org/@ng-web-apis/common/-/common-4.12.0.tgz",
"integrity": "sha512-fXbcMrd/+L+9j9knbgXbDwYe30H4Wt0hQzvqyhpXTVrc0jYwlk3MJTYrnazKz5HvP9318caEv5n4qt3HMf5uPQ==", "integrity": "sha512-OG4ChsEWQ0IbGJ+WrJAiOY5X4jF8f5YUCss961taPeiyhvwtUo4zAuX3UvtV/iJSt8XZ41jaOYFTyMIBGubv4Q==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@@ -3495,9 +3495,9 @@
} }
}, },
"node_modules/@ng-web-apis/intersection-observer": { "node_modules/@ng-web-apis/intersection-observer": {
"version": "4.11.1", "version": "4.12.0",
"resolved": "https://registry.npmjs.org/@ng-web-apis/intersection-observer/-/intersection-observer-4.11.1.tgz", "resolved": "https://registry.npmjs.org/@ng-web-apis/intersection-observer/-/intersection-observer-4.12.0.tgz",
"integrity": "sha512-KjODVVx20yG/U5bnPvp5voihL5DSVFuYwZVY9DNRvaFIcQPMy1tL1t9/oJOdxj7zUSFDL8+Z0RoJbsvArezuSg==", "integrity": "sha512-7MW0y6BrjLKCTUGb5YfsEPYpn17wPmGj8J+2A980ntGNveo9+DILz3KpFHkpS6G6bJGtnD36exU2YvfTUKiyXA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@@ -3505,13 +3505,13 @@
}, },
"peerDependencies": { "peerDependencies": {
"@angular/core": ">=16.0.0", "@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": { "node_modules/@ng-web-apis/mutation-observer": {
"version": "4.11.1", "version": "4.12.0",
"resolved": "https://registry.npmjs.org/@ng-web-apis/mutation-observer/-/mutation-observer-4.11.1.tgz", "resolved": "https://registry.npmjs.org/@ng-web-apis/mutation-observer/-/mutation-observer-4.12.0.tgz",
"integrity": "sha512-YFnkGFE0gd03q4HBJL+WPl3YZRZNq7dVV8yD5uqr0ZCDgmOAMBilrp42FuHBPaYkF73Rs2EpKsKHWz1jASqBbQ==", "integrity": "sha512-Yyz0jpQoGgWzQEhjRH8xC9Umxr2W0hYktuYDNxrmnr6GnBejcfkE+wCon7FhCt6h7BrqR3+Z4cg7TvnyUVHU6Q==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@@ -3519,13 +3519,13 @@
}, },
"peerDependencies": { "peerDependencies": {
"@angular/core": ">=16.0.0", "@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": { "node_modules/@ng-web-apis/platform": {
"version": "4.11.1", "version": "4.12.0",
"resolved": "https://registry.npmjs.org/@ng-web-apis/platform/-/platform-4.11.1.tgz", "resolved": "https://registry.npmjs.org/@ng-web-apis/platform/-/platform-4.12.0.tgz",
"integrity": "sha512-BrhkUIEEAD7wcwR65LSXHYOD6L3IvAb4aV94S8tzxUNeGUPwikX5glQJBT1UwkHWXQjANPKTCNyK1LO+cMPgkw==", "integrity": "sha512-OuwV9OERPvQD+QxS2q84pWg60GlL9O66zl0VBLDR8ARMslBfCg1m70LlbLfQI4mlt4QZzTliUVps1JaGyAEKYA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@@ -3533,9 +3533,9 @@
} }
}, },
"node_modules/@ng-web-apis/resize-observer": { "node_modules/@ng-web-apis/resize-observer": {
"version": "4.11.1", "version": "4.12.0",
"resolved": "https://registry.npmjs.org/@ng-web-apis/resize-observer/-/resize-observer-4.11.1.tgz", "resolved": "https://registry.npmjs.org/@ng-web-apis/resize-observer/-/resize-observer-4.12.0.tgz",
"integrity": "sha512-q8eJ6sovnMhfqIULN1yyhqT35Y2a60vB42p9CUBWPeeVammU+QHE/imPCMCJgnti9cdPZfnyI/+TeYNIUg7mzg==", "integrity": "sha512-ekLcZnqap9OBcoTtOD0/tcOy/STFrxSu3WR0yU9yEM0n7S1mOsKOnXIY4PMAxxWV4LMs91P00wzFHfNNnuOS/g==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@@ -3543,13 +3543,13 @@
}, },
"peerDependencies": { "peerDependencies": {
"@angular/core": ">=16.0.0", "@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": { "node_modules/@ng-web-apis/screen-orientation": {
"version": "4.11.1", "version": "4.12.0",
"resolved": "https://registry.npmjs.org/@ng-web-apis/screen-orientation/-/screen-orientation-4.11.1.tgz", "resolved": "https://registry.npmjs.org/@ng-web-apis/screen-orientation/-/screen-orientation-4.12.0.tgz",
"integrity": "sha512-HS/kWTgVjXVDqMLcJbl5uty+1sV10m9PeDag74tzktIDAB06diFQJQGfcQaA0o0IBisT3fOysf9gHV5sXxSOFw==", "integrity": "sha512-oqZqc9TTpEBNNinVSFjEl/plhoRxh07zMtdG6VMnuY6Lng2l1jfC7vKru2rjEskR+0sYgN8Y8Ttov03i8I9GHg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@@ -3557,7 +3557,7 @@
}, },
"peerDependencies": { "peerDependencies": {
"@angular/core": ">=16.0.0", "@angular/core": ">=16.0.0",
"@ng-web-apis/common": ">=4.11.1", "@ng-web-apis/common": ">=4.12.0",
"rxjs": ">=7.0.0" "rxjs": ">=7.0.0"
} }
}, },
@@ -4418,9 +4418,9 @@
"link": true "link": true
}, },
"node_modules/@taiga-ui/addon-charts": { "node_modules/@taiga-ui/addon-charts": {
"version": "4.30.0", "version": "4.32.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-charts/-/addon-charts-4.30.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/addon-charts/-/addon-charts-4.32.0.tgz",
"integrity": "sha512-QrM2Oh4hUcg/I0K3KWFkc/dbTCYZn2n5GU2FSpZaK6I7pwjfRoMjBU7vswPLVVdmgeWTJxxoQlbfYnbUbkMAJw==", "integrity": "sha512-VhGkBxwfra5eijSvZdXhMKOWEnFMESo5TX3OfsahIXWJXivwguvIc63rIhHYq2uC+t5sj1kINveO4yLqOeAm/Q==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": ">=2.8.1" "tslib": ">=2.8.1"
@@ -4428,16 +4428,16 @@
"peerDependencies": { "peerDependencies": {
"@angular/common": ">=16.0.0", "@angular/common": ">=16.0.0",
"@angular/core": ">=16.0.0", "@angular/core": ">=16.0.0",
"@ng-web-apis/common": "^4.11.1", "@ng-web-apis/common": "^4.12.0",
"@taiga-ui/cdk": "^4.30.0", "@taiga-ui/cdk": "^4.32.0",
"@taiga-ui/core": "^4.30.0", "@taiga-ui/core": "^4.32.0",
"@taiga-ui/polymorpheus": "^4.9.0" "@taiga-ui/polymorpheus": "^4.9.0"
} }
}, },
"node_modules/@taiga-ui/addon-commerce": { "node_modules/@taiga-ui/addon-commerce": {
"version": "4.30.0", "version": "4.32.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-commerce/-/addon-commerce-4.30.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/addon-commerce/-/addon-commerce-4.32.0.tgz",
"integrity": "sha512-6diktxvxMpWjbEHXThS0pTrURdUiF/47jf2jdBFkMwX3BbbekisM1qkwxY24V7q8fN0IIxfO8CVEjTeLRrCw5g==", "integrity": "sha512-AC3VU/RVTNapS8ltSAemZPeDb2CopJEj298rI3Vl4qER1oVl0zunmWVy5ncwK1F1zWKU2/QNDjjo8yKYWeU/Nw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": ">=2.8.1" "tslib": ">=2.8.1"
@@ -4449,19 +4449,19 @@
"@maskito/angular": "^3.5.0", "@maskito/angular": "^3.5.0",
"@maskito/core": "^3.5.0", "@maskito/core": "^3.5.0",
"@maskito/kit": "^3.5.0", "@maskito/kit": "^3.5.0",
"@ng-web-apis/common": "^4.11.1", "@ng-web-apis/common": "^4.12.0",
"@taiga-ui/cdk": "^4.30.0", "@taiga-ui/cdk": "^4.32.0",
"@taiga-ui/core": "^4.30.0", "@taiga-ui/core": "^4.32.0",
"@taiga-ui/i18n": "^4.30.0", "@taiga-ui/i18n": "^4.32.0",
"@taiga-ui/kit": "^4.30.0", "@taiga-ui/kit": "^4.32.0",
"@taiga-ui/polymorpheus": "^4.9.0", "@taiga-ui/polymorpheus": "^4.9.0",
"rxjs": ">=7.0.0" "rxjs": ">=7.0.0"
} }
}, },
"node_modules/@taiga-ui/addon-mobile": { "node_modules/@taiga-ui/addon-mobile": {
"version": "4.30.0", "version": "4.32.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-mobile/-/addon-mobile-4.30.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/addon-mobile/-/addon-mobile-4.32.0.tgz",
"integrity": "sha512-8cYyU0UDLUd74v+Zjs4m9S4AsSWchUojAexDLvaAHzfi0x+tdtA+ZN0h49v8AmOWHK0v69z4FMjyyc52p/jiDw==", "integrity": "sha512-pUoHWyILPj6KIAhna1JDzz48c2nCjqYb1tb7AL3LQ3qfNwAbg9fvjBIfrgWMhW0LaDeh5+FfrS7oiO/ERcHTLg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": ">=2.8.1" "tslib": ">=2.8.1"
@@ -4470,19 +4470,19 @@
"@angular/cdk": ">=16.0.0", "@angular/cdk": ">=16.0.0",
"@angular/common": ">=16.0.0", "@angular/common": ">=16.0.0",
"@angular/core": ">=16.0.0", "@angular/core": ">=16.0.0",
"@ng-web-apis/common": "^4.11.1", "@ng-web-apis/common": "^4.12.0",
"@taiga-ui/cdk": "^4.30.0", "@taiga-ui/cdk": "^4.32.0",
"@taiga-ui/core": "^4.30.0", "@taiga-ui/core": "^4.32.0",
"@taiga-ui/kit": "^4.30.0", "@taiga-ui/kit": "^4.32.0",
"@taiga-ui/layout": "^4.30.0", "@taiga-ui/layout": "^4.32.0",
"@taiga-ui/polymorpheus": "^4.9.0", "@taiga-ui/polymorpheus": "^4.9.0",
"rxjs": ">=7.0.0" "rxjs": ">=7.0.0"
} }
}, },
"node_modules/@taiga-ui/addon-table": { "node_modules/@taiga-ui/addon-table": {
"version": "4.30.0", "version": "4.32.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-table/-/addon-table-4.30.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/addon-table/-/addon-table-4.32.0.tgz",
"integrity": "sha512-OdCEwlrMs42Z2pINK1wvNk7OZmAlkj+mbgHTyMGdrUdA49dFZfYXNpVUCwVOqHAm2PDOeVN4ybZ8FSbzYefJyw==", "integrity": "sha512-8oXeqLO1wGH8RYHTYWhjCvrKWptPN1we04NRahmFY4AxSJ3u7MqaR4420RRNO4zZG9kGyktLXPjqGocMoymL8Q==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": ">=2.8.1" "tslib": ">=2.8.1"
@@ -4490,19 +4490,19 @@
"peerDependencies": { "peerDependencies": {
"@angular/common": ">=16.0.0", "@angular/common": ">=16.0.0",
"@angular/core": ">=16.0.0", "@angular/core": ">=16.0.0",
"@ng-web-apis/intersection-observer": "^4.11.1", "@ng-web-apis/intersection-observer": "^4.12.0",
"@taiga-ui/cdk": "^4.30.0", "@taiga-ui/cdk": "^4.32.0",
"@taiga-ui/core": "^4.30.0", "@taiga-ui/core": "^4.32.0",
"@taiga-ui/i18n": "^4.30.0", "@taiga-ui/i18n": "^4.32.0",
"@taiga-ui/kit": "^4.30.0", "@taiga-ui/kit": "^4.32.0",
"@taiga-ui/polymorpheus": "^4.9.0", "@taiga-ui/polymorpheus": "^4.9.0",
"rxjs": ">=7.0.0" "rxjs": ">=7.0.0"
} }
}, },
"node_modules/@taiga-ui/cdk": { "node_modules/@taiga-ui/cdk": {
"version": "4.30.0", "version": "4.32.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-4.30.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-4.32.0.tgz",
"integrity": "sha512-ndfnLOnL6vriItm5lq8/0slzj03CatkGVYG8zAT3fx00Vuam5Wf8Sh6h2ObqCFAljT7WJxHqMF9A1cBfLPI/iQ==", "integrity": "sha512-qvYe79uh6Tw2LJSEGLJYUlAidbZi6JgcuMRqWAB1JhyIGpgnaqar5v+oJJg28zJZZ81PCj59VkFNLL0dNVXRUg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": "2.8.1" "tslib": "2.8.1"
@@ -4520,20 +4520,20 @@
"@angular/common": ">=16.0.0", "@angular/common": ">=16.0.0",
"@angular/core": ">=16.0.0", "@angular/core": ">=16.0.0",
"@angular/forms": ">=16.0.0", "@angular/forms": ">=16.0.0",
"@ng-web-apis/common": "^4.11.1", "@ng-web-apis/common": "^4.12.0",
"@ng-web-apis/mutation-observer": "^4.11.1", "@ng-web-apis/mutation-observer": "^4.12.0",
"@ng-web-apis/platform": "^4.11.1", "@ng-web-apis/platform": "^4.12.0",
"@ng-web-apis/resize-observer": "^4.11.1", "@ng-web-apis/resize-observer": "^4.12.0",
"@ng-web-apis/screen-orientation": "^4.11.1", "@ng-web-apis/screen-orientation": "^4.12.0",
"@taiga-ui/event-plugins": "^4.4.1", "@taiga-ui/event-plugins": "^4.5.1",
"@taiga-ui/polymorpheus": "^4.9.0", "@taiga-ui/polymorpheus": "^4.9.0",
"rxjs": ">=7.0.0" "rxjs": ">=7.0.0"
} }
}, },
"node_modules/@taiga-ui/core": { "node_modules/@taiga-ui/core": {
"version": "4.30.0", "version": "4.32.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-4.30.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-4.32.0.tgz",
"integrity": "sha512-IeZ6QBpSuv7k4bQx2BSDr8N3dDiMDwgnnwkkKqtJ0yJayZ/ZlCMq3nUQA0kg3VjH2spJeUbdqkDqpEuzrWJGkA==", "integrity": "sha512-e1z7YhhjePMRLTk+s83OclN45wMixCwZWMxM9WuXIyd2KXMPhJvrrgBDjoK66GuFtjZ4qaSF/H2FTIJmJ/6MiQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": ">=2.8.1" "tslib": ">=2.8.1"
@@ -4545,19 +4545,19 @@
"@angular/forms": ">=16.0.0", "@angular/forms": ">=16.0.0",
"@angular/platform-browser": ">=16.0.0", "@angular/platform-browser": ">=16.0.0",
"@angular/router": ">=16.0.0", "@angular/router": ">=16.0.0",
"@ng-web-apis/common": "^4.11.1", "@ng-web-apis/common": "^4.12.0",
"@ng-web-apis/mutation-observer": "^4.11.1", "@ng-web-apis/mutation-observer": "^4.12.0",
"@taiga-ui/cdk": "^4.30.0", "@taiga-ui/cdk": "^4.32.0",
"@taiga-ui/event-plugins": "^4.4.1", "@taiga-ui/event-plugins": "^4.5.1",
"@taiga-ui/i18n": "^4.30.0", "@taiga-ui/i18n": "^4.32.0",
"@taiga-ui/polymorpheus": "^4.9.0", "@taiga-ui/polymorpheus": "^4.9.0",
"rxjs": ">=7.0.0" "rxjs": ">=7.0.0"
} }
}, },
"node_modules/@taiga-ui/event-plugins": { "node_modules/@taiga-ui/event-plugins": {
"version": "4.5.0", "version": "4.5.1",
"resolved": "https://registry.npmjs.org/@taiga-ui/event-plugins/-/event-plugins-4.5.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/event-plugins/-/event-plugins-4.5.1.tgz",
"integrity": "sha512-bMW36eqr4Q+EnUM8ZNjx1Sw8POIAcyALY74xVPq9UHoQ3NqnRkeEDnZdfPhq9IYxtC3sO2BttNjWYcvBAkU2+A==", "integrity": "sha512-p5TAs6ZAJAlyl64OUdvnVnfCvDteJtLl9cjXarMzPRY7sX2d+SC97qnTZF8ACPcx4FgFaiHI6VH5G6UzYHMZ8g==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
@@ -4569,9 +4569,9 @@
} }
}, },
"node_modules/@taiga-ui/experimental": { "node_modules/@taiga-ui/experimental": {
"version": "4.30.0", "version": "4.32.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/experimental/-/experimental-4.30.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/experimental/-/experimental-4.32.0.tgz",
"integrity": "sha512-0GWkBinW+tqQIFkWQbTqMBTkKGZhju3RslKRCYbjal/hfcIuSAsEPZqLqIQqVqJNz6AhaIpT0UQ+I7QXzx1/yw==", "integrity": "sha512-sCOasTF9UlgPOW4vXSeM5M1tgGrjgofa+Qq7hejYW3BXs/4mnmdm5yiYzWfMVZd4jgTSeV5kobkIJ9Fkp2zt6g==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": ">=2.8.1" "tslib": ">=2.8.1"
@@ -4579,18 +4579,18 @@
"peerDependencies": { "peerDependencies": {
"@angular/common": ">=16.0.0", "@angular/common": ">=16.0.0",
"@angular/core": ">=16.0.0", "@angular/core": ">=16.0.0",
"@taiga-ui/addon-commerce": "^4.30.0", "@taiga-ui/addon-commerce": "^4.32.0",
"@taiga-ui/cdk": "^4.30.0", "@taiga-ui/cdk": "^4.32.0",
"@taiga-ui/core": "^4.30.0", "@taiga-ui/core": "^4.32.0",
"@taiga-ui/kit": "^4.30.0", "@taiga-ui/kit": "^4.32.0",
"@taiga-ui/polymorpheus": "^4.9.0", "@taiga-ui/polymorpheus": "^4.9.0",
"rxjs": ">=7.0.0" "rxjs": ">=7.0.0"
} }
}, },
"node_modules/@taiga-ui/i18n": { "node_modules/@taiga-ui/i18n": {
"version": "4.30.0", "version": "4.32.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-4.30.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-4.32.0.tgz",
"integrity": "sha512-OvtUqSRQE988XfiH1MS7Wd3Eg6dE1mkP2sqYRLw0HyE5Oc9hgHMwdPstSaoMN9aeJRVZnKXGsYmX4iaQ3x7drw==", "integrity": "sha512-PAQv9RxSgvf3RBUps9bXX2erCk9oiSt9ApM3SAIa/OuzET0TJsW6yZ4EQrtLw03bMX3wyA8PnEYva9wzoYAqxA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@@ -4598,23 +4598,23 @@
}, },
"peerDependencies": { "peerDependencies": {
"@angular/core": ">=16.0.0", "@angular/core": ">=16.0.0",
"@ng-web-apis/common": "^4.11.1", "@ng-web-apis/common": "^4.12.0",
"rxjs": ">=7.0.0" "rxjs": ">=7.0.0"
} }
}, },
"node_modules/@taiga-ui/icons": { "node_modules/@taiga-ui/icons": {
"version": "4.30.0", "version": "4.32.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/icons/-/icons-4.30.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/icons/-/icons-4.32.0.tgz",
"integrity": "sha512-EAbvw1ii4UVDgt9+5t7NQkV0WBqkVm5SGixH0ux8Vb4qhhLJJwp5xvXOCGt5QPzviT7nFGqXD6EqB23aYcuusg==", "integrity": "sha512-X2ZSiqeMKigULgX91fBZkFJRUbwzeW934yLEGhq7C1JMcC2+ppLmL/NbkD2kpKZ4OeHnGsItxKauoXu44rXeLA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
} }
}, },
"node_modules/@taiga-ui/kit": { "node_modules/@taiga-ui/kit": {
"version": "4.30.0", "version": "4.32.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-4.30.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-4.32.0.tgz",
"integrity": "sha512-tCHZbsiq1u19ariarFuP9iwnNSxJGicQnYvJYy2+QojL65KsC9p8VgZv36rpggpuPEUXRXwmhyz2Qi6fwFcbLg==", "integrity": "sha512-J8XoqeQHBNbAAuTz0kVACujDOb3zuh4Vps83lYl+msFIaUmkjC37muXF3eRlImH3m4DpT8yI8+ffh/T3+jky7w==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": ">=2.8.1" "tslib": ">=2.8.1"
@@ -4628,21 +4628,21 @@
"@maskito/core": "^3.5.0", "@maskito/core": "^3.5.0",
"@maskito/kit": "^3.5.0", "@maskito/kit": "^3.5.0",
"@maskito/phone": "^3.5.0", "@maskito/phone": "^3.5.0",
"@ng-web-apis/common": "^4.11.1", "@ng-web-apis/common": "^4.12.0",
"@ng-web-apis/intersection-observer": "^4.11.1", "@ng-web-apis/intersection-observer": "^4.12.0",
"@ng-web-apis/mutation-observer": "^4.11.1", "@ng-web-apis/mutation-observer": "^4.12.0",
"@ng-web-apis/resize-observer": "^4.11.1", "@ng-web-apis/resize-observer": "^4.12.0",
"@taiga-ui/cdk": "^4.30.0", "@taiga-ui/cdk": "^4.32.0",
"@taiga-ui/core": "^4.30.0", "@taiga-ui/core": "^4.32.0",
"@taiga-ui/i18n": "^4.30.0", "@taiga-ui/i18n": "^4.32.0",
"@taiga-ui/polymorpheus": "^4.9.0", "@taiga-ui/polymorpheus": "^4.9.0",
"rxjs": ">=7.0.0" "rxjs": ">=7.0.0"
} }
}, },
"node_modules/@taiga-ui/layout": { "node_modules/@taiga-ui/layout": {
"version": "4.30.0", "version": "4.32.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/layout/-/layout-4.30.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/layout/-/layout-4.32.0.tgz",
"integrity": "sha512-DyIqpmXcv/OP4byt7L1f1iBKPysf3L+sj/dBpkeYvAUUnJnXnJsXav0j57d43VkXPn9lpGqz0gEBtzVDt7xxTw==", "integrity": "sha512-ECaoJ3CbL+eoqL1MleaHvD9/FQ5OCaUMkjOdXId2Jg2MNbuDhtS9hqVZvSWLXRWz3XgC3aADYnPwrNvIsy5Mng==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": ">=2.8.1" "tslib": ">=2.8.1"
@@ -4650,17 +4650,17 @@
"peerDependencies": { "peerDependencies": {
"@angular/common": ">=16.0.0", "@angular/common": ">=16.0.0",
"@angular/core": ">=16.0.0", "@angular/core": ">=16.0.0",
"@taiga-ui/cdk": "^4.30.0", "@taiga-ui/cdk": "^4.32.0",
"@taiga-ui/core": "^4.30.0", "@taiga-ui/core": "^4.32.0",
"@taiga-ui/kit": "^4.30.0", "@taiga-ui/kit": "^4.32.0",
"@taiga-ui/polymorpheus": "^4.9.0", "@taiga-ui/polymorpheus": "^4.9.0",
"rxjs": ">=7.0.0" "rxjs": ">=7.0.0"
} }
}, },
"node_modules/@taiga-ui/legacy": { "node_modules/@taiga-ui/legacy": {
"version": "4.30.0", "version": "4.32.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/legacy/-/legacy-4.30.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/legacy/-/legacy-4.32.0.tgz",
"integrity": "sha512-ebFJMddzlsq3TUAWxopn5Qju4REkC4bHzoYYx5OEzPq1VW1zmCvNC+X6usMnluhc9aS50UI8ZB7Xd3N4Zdgtfg==", "integrity": "sha512-wEsywt6hK2NNpHddqVrL0MTd1QFzmhMdPPgtraNOieQmzrSW2jpA37KJO11cVleuRdDsk98rFtzQ3stlNNFy5Q==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": ">=2.8.1" "tslib": ">=2.8.1"

View File

@@ -47,18 +47,18 @@
"@noble/hashes": "^1.4.0", "@noble/hashes": "^1.4.0",
"@start9labs/argon2": "^0.2.2", "@start9labs/argon2": "^0.2.2",
"@start9labs/start-sdk": "file:../sdk/baseDist", "@start9labs/start-sdk": "file:../sdk/baseDist",
"@taiga-ui/addon-charts": "4.30.0", "@taiga-ui/addon-charts": "4.32.0",
"@taiga-ui/addon-commerce": "4.30.0", "@taiga-ui/addon-commerce": "4.32.0",
"@taiga-ui/addon-mobile": "4.30.0", "@taiga-ui/addon-mobile": "4.32.0",
"@taiga-ui/addon-table": "4.30.0", "@taiga-ui/addon-table": "4.32.0",
"@taiga-ui/cdk": "4.30.0", "@taiga-ui/cdk": "4.32.0",
"@taiga-ui/core": "4.30.0", "@taiga-ui/core": "4.32.0",
"@taiga-ui/event-plugins": "4.5.0", "@taiga-ui/event-plugins": "4.5.1",
"@taiga-ui/experimental": "4.30.0", "@taiga-ui/experimental": "4.32.0",
"@taiga-ui/icons": "4.30.0", "@taiga-ui/icons": "4.32.0",
"@taiga-ui/kit": "4.30.0", "@taiga-ui/kit": "4.32.0",
"@taiga-ui/layout": "4.30.0", "@taiga-ui/layout": "4.32.0",
"@taiga-ui/legacy": "4.30.0", "@taiga-ui/legacy": "4.32.0",
"@taiga-ui/polymorpheus": "4.9.0", "@taiga-ui/polymorpheus": "4.9.0",
"@tinkoff/ng-dompurify": "4.0.0", "@tinkoff/ng-dompurify": "4.0.0",
"ansi-to-html": "^0.7.2", "ansi-to-html": "^0.7.2",

View File

@@ -8,7 +8,7 @@
header { header {
@include scrollbar-hidden(); @include scrollbar-hidden();
// TODO: Theme // @TODO Theme
background: #2b2b2f; background: #2b2b2f;
overflow: hidden; overflow: hidden;
width: 100%; width: 100%;

View File

@@ -16,21 +16,3 @@
</span> </span>
</div> </div>
</div> </div>
<!-- @TODO Alex -->
<!-- <ion-item
class="service-card"
[routerLink]="['/marketplace', pkg.id]"
[queryParams]="{ flavor: pkg.flavor, version: pkg.version }"
>
<ion-thumbnail slot="start">
<img alt="" [src]="pkg.icon | trustUrl" />
</ion-thumbnail>
<ion-label>
<h2 class="montserrat">
<strong>{{ pkg.title }}</strong>
</h2>
<h3>{{ pkg.description.short }}</h3>
<ng-content></ng-content>
</ion-label>
</ion-item> -->

View File

@@ -6,7 +6,7 @@ import {
} from '@angular/core' } from '@angular/core'
import { TuiDialogService } from '@taiga-ui/core' import { TuiDialogService } from '@taiga-ui/core'
import { RELEASE_NOTES } from '../../../modals/release-notes.component' import { RELEASE_NOTES } from '../../../modals/release-notes.component'
import { MarketplacePkg } from '../../../types' import { MarketplacePkgBase } from '../../../types'
@Component({ @Component({
selector: 'marketplace-about', selector: 'marketplace-about',
@@ -18,7 +18,7 @@ export class AboutComponent {
private readonly dialogs = inject(TuiDialogService) private readonly dialogs = inject(TuiDialogService)
@Input({ required: true }) @Input({ required: true })
pkg!: MarketplacePkg pkg!: MarketplacePkgBase
async onPast() { async onPast() {
this.dialogs this.dialogs

View File

@@ -1,6 +1,6 @@
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, Input } from '@angular/core' 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' import { TuiLineClamp } from '@taiga-ui/kit'
@Component({ @Component({
@@ -43,7 +43,7 @@ import { TuiLineClamp } from '@taiga-ui/kit'
], ],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true, standalone: true,
imports: [CommonModule, TuiLineClamp, TuiLabel, TuiIcon, TuiTitle], imports: [CommonModule, TuiLineClamp, TuiIcon, TuiTitle],
}) })
export class MarketplaceAdditionalItemComponent { export class MarketplaceAdditionalItemComponent {
@Input({ required: true }) @Input({ required: true })

View File

@@ -4,7 +4,7 @@
<div class="detail-container"> <div class="detail-container">
<!-- release date --> <!-- release date -->
<marketplace-additional-item <marketplace-additional-item
*ngIf="pkg.s9pk.publishedAt as published" *ngIf="pkg.s9pk?.publishedAt as published"
[data]="(published | date: 'medium')!" [data]="(published | date: 'medium')!"
label="Released" label="Released"
icon="" icon=""
@@ -76,98 +76,3 @@
</div> </div>
</div> </div>
</div> </div>
<!-- <ion-item-divider>Additional Info</ion-item-divider>
<ion-grid *ngIf="pkg">
<ion-row>
<ion-col responsiveCol sizeXs="12" sizeMd="6">
<ion-item-group>
<ion-item
*ngIf="pkg.gitHash as gitHash; else noHash"
button
detail="false"
(click)="copy(gitHash)"
>
<ion-label>
<h2>Git Hash</h2>
<p>{{ gitHash }}</p>
</ion-label>
<ion-icon slot="end" name="copy-outline"></ion-icon>
</ion-item>
<ng-template #noHash>
<ion-item>
<ion-label>
<h2>Git Hash</h2>
<p>Unknown</p>
</ion-label>
</ion-item>
</ng-template>
<ion-item button detail="false" (click)="presentAlertVersions()">
<ion-label>
<h2>Other Versions</h2>
<p>Click to view other versions</p>
</ion-label>
<ion-icon slot="end" name="chevron-forward"></ion-icon>
</ion-item>
<ion-item button detail="false" (click)="presentModalMd('license')">
<ion-label>
<h2>License</h2>
<p>{{ pkg.license }}</p>
</ion-label>
<ion-icon slot="end" name="chevron-forward"></ion-icon>
</ion-item>
<ion-item
button
detail="false"
(click)="presentModalMd('instructions')"
>
<ion-label>
<h2>Instructions</h2>
<p>Click to view instructions</p>
</ion-label>
<ion-icon slot="end" name="chevron-forward"></ion-icon>
</ion-item>
</ion-item-group>
</ion-col>
<ion-col responsiveCol sizeXs="12" sizeMd="6">
<ion-item-group>
<ion-item
[href]="pkg.upstreamRepo"
target="_blank"
rel="noreferrer"
detail="false"
>
<ion-label>
<h2>Source Repository</h2>
<p>{{ pkg.upstreamRepo }}</p>
</ion-label>
<ion-icon slot="end" name="open-outline"></ion-icon>
</ion-item>
<ion-item
[href]="pkg.wrapperRepo"
target="_blank"
rel="noreferrer"
detail="false"
>
<ion-label>
<h2>Wrapper Repository</h2>
<p>{{ pkg.wrapperRepo }}</p>
</ion-label>
<ion-icon slot="end" name="open-outline"></ion-icon>
</ion-item>
<ion-item
[href]="pkg.supportSite"
[disabled]="!pkg.supportSite"
target="_blank"
rel="noreferrer"
detail="false"
>
<ion-label>
<h2>Support Site</h2>
<p>{{ pkg.supportSite || 'Not provided' }}</p>
</ion-label>
<ion-icon slot="end" name="open-outline"></ion-icon>
</ion-item>
</ion-item-group>
</ion-col>
</ion-row>
</ion-grid> -->

View File

@@ -7,8 +7,7 @@ import {
} from '@angular/core' } from '@angular/core'
import { ActivatedRoute } from '@angular/router' import { ActivatedRoute } from '@angular/router'
import { CopyService } from '@start9labs/shared' import { CopyService } from '@start9labs/shared'
import { TuiDialogService } from '@taiga-ui/core' import { MarketplacePkgBase } from '../../../types'
import { MarketplacePkg } from '../../../types'
@Component({ @Component({
selector: 'marketplace-additional', selector: 'marketplace-additional',
@@ -18,14 +17,13 @@ import { MarketplacePkg } from '../../../types'
}) })
export class AdditionalComponent { export class AdditionalComponent {
@Input({ required: true }) @Input({ required: true })
pkg!: MarketplacePkg pkg!: MarketplacePkgBase
@Output() @Output()
readonly static = new EventEmitter<string>() readonly static = new EventEmitter<'License' | 'Instructions'>()
constructor( constructor(
readonly copyService: CopyService, readonly copyService: CopyService,
private readonly dialogs: TuiDialogService,
private readonly route: ActivatedRoute, private readonly route: ActivatedRoute,
) {} ) {}

View File

@@ -6,7 +6,7 @@ import {
Input, Input,
Output, Output,
} from '@angular/core' } from '@angular/core'
import { MarketplacePkg } from '../../../types' import { MarketplacePkgBase } from '../../../types'
import { MarketplaceDepItemComponent } from './dependency-item.component' import { MarketplaceDepItemComponent } from './dependency-item.component'
@Component({ @Component({
@@ -55,7 +55,7 @@ import { MarketplaceDepItemComponent } from './dependency-item.component'
}) })
export class MarketplaceDependenciesComponent { export class MarketplaceDependenciesComponent {
@Input({ required: true }) @Input({ required: true })
pkg!: MarketplacePkg pkg!: MarketplacePkgBase
@Output() open = new EventEmitter<string>() @Output() open = new EventEmitter<string>()
} }

View File

@@ -3,9 +3,8 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { RouterModule } from '@angular/router' import { RouterModule } from '@angular/router'
import { ExverPipesModule } from '@start9labs/shared' import { ExverPipesModule } from '@start9labs/shared'
import { T } from '@start9labs/start-sdk' import { T } from '@start9labs/start-sdk'
import { TuiLet } from '@taiga-ui/cdk'
import { TuiAvatar, TuiLineClamp } from '@taiga-ui/kit' import { TuiAvatar, TuiLineClamp } from '@taiga-ui/kit'
import { MarketplacePkg } from '../../../types' import { MarketplacePkgBase } from '../../../types'
@Component({ @Component({
selector: 'marketplace-dep-item', selector: 'marketplace-dep-item',
@@ -97,12 +96,11 @@ import { MarketplacePkg } from '../../../types'
TuiAvatar, TuiAvatar,
ExverPipesModule, ExverPipesModule,
TuiLineClamp, TuiLineClamp,
TuiLet,
], ],
}) })
export class MarketplaceDepItemComponent { export class MarketplaceDepItemComponent {
@Input({ required: true }) @Input({ required: true })
pkg!: MarketplacePkg pkg!: MarketplacePkgBase
@Input({ required: true }) @Input({ required: true })
dep!: KeyValue<string, T.DependencyMetadata> dep!: KeyValue<string, T.DependencyMetadata>

View File

@@ -1,3 +1,4 @@
import { OptionalProperty } from '@start9labs/shared'
import { T } from '@start9labs/start-sdk' import { T } from '@start9labs/start-sdk'
export type GetPackageReq = { export type GetPackageReq = {
@@ -31,11 +32,17 @@ export type StoreData = {
packages: MarketplacePkg[] packages: MarketplacePkg[]
} }
export type MarketplacePkg = T.PackageVersionInfo & export type MarketplacePkgBase = OptionalProperty<
Omit<GetPackageRes, 'best'> & { T.PackageVersionInfo,
id: T.PackageId 's9pk'
version: string > & {
flavor: string | null id: T.PackageId
} version: string
flavor: string | null
}
export type MarketplacePkg = MarketplacePkgBase &
GetPackageRes &
T.PackageVersionInfo
export type StoreDataWithUrl = StoreData & { url: string } export type StoreDataWithUrl = StoreData & { url: string }

View File

@@ -117,7 +117,7 @@ export class MockApiService extends ApiService {
})), })),
) as Observable<T> ) as Observable<T>
} else if (guid === 'progress-guid') { } else if (guid === 'progress-guid') {
// @TODO mock progress // @TODO Matt mock progress
return interval(1000).pipe( return interval(1000).pipe(
map(() => ({ map(() => ({
overall: true, overall: true,

View File

@@ -1,6 +1,5 @@
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { TuiLet } from '@taiga-ui/cdk'
import { TuiProgress } from '@taiga-ui/kit' import { TuiProgress } from '@taiga-ui/kit'
import { LogsWindowComponent } from './logs-window.component' import { LogsWindowComponent } from './logs-window.component'
@@ -31,7 +30,7 @@ import { LogsWindowComponent } from './logs-window.component'
padding: 1rem; padding: 1rem;
margin: 1.5rem; margin: 1.5rem;
text-align: center; text-align: center;
/* TODO: Theme */ // @TODO Theme
background: #e0e0e0; background: #e0e0e0;
color: #333; color: #333;
--tui-background-neutral-1: rgba(0, 0, 0, 0.1); --tui-background-neutral-1: rgba(0, 0, 0, 0.1);
@@ -46,11 +45,11 @@ import { LogsWindowComponent } from './logs-window.component'
text-align: left; text-align: left;
overflow: hidden; overflow: hidden;
border-radius: 2rem; border-radius: 2rem;
/* TODO: Theme */ // @TODO Theme
background: #181818; background: #181818;
} }
`, `,
imports: [CommonModule, LogsWindowComponent, TuiLet, TuiProgress], imports: [CommonModule, LogsWindowComponent, TuiProgress],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class InitializingComponent { export class InitializingComponent {

View File

@@ -1,7 +1,7 @@
import { AfterViewInit, Directive, ElementRef, Inject } from '@angular/core' import { AfterViewInit, Directive, ElementRef, Inject } from '@angular/core'
import { DOCUMENT } from '@angular/common' 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({ @Directive({
selector: '[safeLinks]', selector: '[safeLinks]',
standalone: true, standalone: true,

View File

@@ -2,7 +2,7 @@ import { ErrorHandler, inject, Injectable } from '@angular/core'
import { TuiAlertService } from '@taiga-ui/core' import { TuiAlertService } from '@taiga-ui/core'
import { HttpError } from '../classes/http-error' import { HttpError } from '../classes/http-error'
// TODO: Enable this as ErrorHandler // @TODO Alex: Enable this as ErrorHandler
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })

View File

@@ -1,5 +1,13 @@
import { StaticClassProvider } from '@angular/core' 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 { FollowLogsReq, FollowLogsRes, Log } from '../types/api'
import { Constructor } from '../types/constructor' import { Constructor } from '../types/constructor'
import { convertAnsi } from '../util/convert-ansi' import { convertAnsi } from '../util/convert-ansi'
@@ -22,7 +30,8 @@ export function provideSetupLogsService(
export class SetupLogsService extends Observable<readonly string[]> { export class SetupLogsService extends Observable<readonly string[]> {
private readonly log$ = defer(() => this.api.followServerLogs({})).pipe( private readonly log$ = defer(() => this.api.followServerLogs({})).pipe(
switchMap(({ guid }) => this.api.openWebsocket$(guid)), switchMap(({ guid }) => this.api.openWebsocket$(guid)),
bufferTime(1000), bufferTime(500),
filter(logs => !!logs.length),
map(convertAnsi), map(convertAnsi),
scan((logs: readonly string[], log) => [...logs, log], []), scan((logs: readonly string[], log) => [...logs, log], []),
) )

View File

@@ -1,22 +1,29 @@
// @TODO get types from sdk import { T } from '@start9labs/start-sdk'
type Progress = null | boolean | { done: number; total: number | null }
type NamedProgress = { name: string; progress: Progress }
type FullProgress = { overall: Progress; phases: Array<NamedProgress> }
export function formatProgress({ phases, overall }: FullProgress): { export function formatProgress({ phases, overall }: T.FullProgress): {
total: number total: number
message: string message: string
} { } {
return { return {
total: getDecimal(overall), total: getDecimal(overall),
message: phases message: phases
.filter(p => p.progress !== true && p.progress !== null) .filter(
.map(p => `${p.name}${getPhaseBytes(p.progress)}`) (
p,
): p is {
name: string
progress: {
done: number
total: number | null
}
} => p.progress !== true && p.progress !== null,
)
.map(p => `<b>${p.name}</b>${getPhaseBytes(p.progress)}`)
.join(', '), .join(', '),
} }
} }
function getDecimal(progress: Progress): number { function getDecimal(progress: T.Progress): number {
if (progress === true) { if (progress === true) {
return 1 return 1
} else if (!progress || !progress.total) { } 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 return progress === true || !progress
? '' ? ''
: `: (${progress.done}/${progress.total})` : `: (${progress.done}/${progress.total})`

View File

@@ -55,3 +55,6 @@ export function toUrl(text: string | null | undefined): string {
} }
export type WithId<T> = T & { id: string } export type WithId<T> = T & { id: string }
export type OptionalProperty<T, K extends keyof T> = Omit<T, K> &
Partial<Pick<T, K>>

View File

@@ -169,7 +169,7 @@ $wide-modal: 900px;
--portal-header-height: 56px; --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-black: 0 0 0;
--tw-color-white: 255 255 255; --tw-color-white: 255 255 255;
--tw-color-slate-50: 248 250 252; --tw-color-slate-50: 248 250 252;

View File

@@ -139,7 +139,7 @@ tui-dropdown[data-appearance='start-os'][data-appearance='start-os'] {
background: rgb(34 34 34 / 80%); background: rgb(34 34 34 / 80%);
} }
// TODO: Move to Taiga UI // @TODO Alex: Move to Taiga UI
a[tuiIconButton]:not([href]) { a[tuiIconButton]:not([href]) {
pointer-events: none; pointer-events: none;
opacity: var(--tui-disabled-opacity); opacity: var(--tui-disabled-opacity);

View File

@@ -9,6 +9,7 @@ import { debounceTime, endWith, map, merge, Subject } from 'rxjs'
import { ConfigService } from 'src/app/services/config.service' import { ConfigService } from 'src/app/services/config.service'
import { DataModel } from 'src/app/services/patch-db/data-model' import { DataModel } from 'src/app/services/patch-db/data-model'
// @TODO Alex
@Component({ @Component({
standalone: true, standalone: true,
selector: 'refresh-alert', selector: 'refresh-alert',

View File

@@ -6,16 +6,7 @@ import {
provideSetupLogsService, provideSetupLogsService,
} from '@start9labs/shared' } from '@start9labs/shared'
import { T } from '@start9labs/start-sdk' import { T } from '@start9labs/start-sdk'
import { import { catchError, defer, from, map, startWith, switchMap, tap } from 'rxjs'
catchError,
defer,
EMPTY,
from,
map,
startWith,
switchMap,
tap,
} from 'rxjs'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { StateService } from 'src/app/services/state.service' import { StateService } from 'src/app/services/state.service'
@@ -36,18 +27,25 @@ export default class InitializingPage {
defer(() => from(this.api.initFollowProgress())).pipe( defer(() => from(this.api.initFollowProgress())).pipe(
switchMap(({ guid, progress }) => switchMap(({ guid, progress }) =>
this.api this.api
.openWebsocket$<T.FullProgress>(guid, {}) .openWebsocket$<T.FullProgress>(guid, {
closeObserver: {
next: () => {
this.state.syncState()
},
},
})
.pipe(startWith(progress)), .pipe(startWith(progress)),
), ),
map(formatProgress), map(formatProgress),
tap<{ total: number; message: string }>(({ total }) => { tap(({ total }) => {
if (total === 1) { if (total === 1) {
this.state.syncState() this.state.syncState()
} }
}), }),
catchError(e => { catchError((e, caught$) => {
console.error(e) console.error(e)
return EMPTY this.state.syncState()
return caught$
}), }),
), ),
{ initialValue: { total: 0, message: '' } }, { initialValue: { total: 0, message: '' } },

View File

@@ -42,7 +42,6 @@ export class LoginPage {
} }
await this.api.login({ await this.api.login({
password: this.password, password: this.password,
metadata: { platforms: [] }, // @TODO do we really need platforms now?
ephemeral: window.location.host === 'localhost', ephemeral: window.location.host === 'localhost',
}) })

View File

@@ -17,7 +17,7 @@ export abstract class Control<
return this.control.spec return this.control.spec
} }
// TODO: Properly handle already set immutable value // @TODO Alex: Properly handle already set immutable value
get readOnly(): boolean { get readOnly(): boolean {
return ( return (
!!this.value && !!this.control.control?.pristine && this.control.immutable !!this.value && !!this.control.control?.pristine && this.control.immutable

View File

@@ -91,7 +91,7 @@ import { HeaderStatusComponent } from './status.component'
} }
&:has([data-status='success']) { &:has([data-status='success']) {
--status: var(--tui-status-positive); --status: transparent;
} }
} }

View File

@@ -88,6 +88,7 @@ import { InterfaceComponent } from './interface.component'
text-align: right; text-align: right;
grid-area: 1 / 2 / 3 / 3; grid-area: 1 / 2 / 3 / 3;
place-content: center; place-content: center;
white-space: nowrap;
} }
.mobile { .mobile {

View File

@@ -15,6 +15,7 @@ import { MappedServiceInterface } from './interface.utils'
`, `,
styles: ` styles: `
:host { :host {
max-width: 56rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;

View File

@@ -0,0 +1,23 @@
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
import { TuiBadge } from '@taiga-ui/kit'
@Component({
standalone: true,
selector: 'interface-status',
template: `
<tui-badge
size="l"
[iconStart]="public() ? '@tui.globe' : '@tui.lock'"
[style.vertical-align.rem]="-0.125"
[style.margin]="'0 0.25rem -0.25rem'"
[appearance]="public() ? 'positive' : 'negative'"
>
{{ public() ? 'Public' : 'Private' }}
</tui-badge>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TuiBadge],
})
export class InterfaceStatusComponent {
readonly public = input(false)
}

View File

@@ -80,7 +80,7 @@ const FILTER = ['/portal/services', '/portal/system', '/portal/marketplace']
:host { :host {
display: none; display: none;
backdrop-filter: blur(1rem); backdrop-filter: blur(1rem);
// TODO: Theme // TODO Theme
--tui-background-elevation-1: #333; --tui-background-elevation-1: #333;
--tui-background-base: #fff; --tui-background-base: #fff;
--tui-border-normal: var(--tui-background-neutral-1); --tui-border-normal: var(--tui-background-neutral-1);

View File

@@ -24,7 +24,7 @@ import { HeaderComponent } from './components/header/header.component'
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
// TODO: Theme // @TODO Theme
background: url(/assets/img/background_dark.jpeg) fixed center/cover; background: url(/assets/img/background_dark.jpeg) fixed center/cover;
} }

View File

@@ -102,7 +102,7 @@ export class BackupsUpcomingComponent {
readonly targets = toSignal(from(this.api.getBackupTargets({}))) readonly targets = toSignal(from(this.api.getBackupTargets({})))
readonly current = toSignal( readonly current = toSignal(
inject<PatchDB<DataModel>>(PatchDB) inject<PatchDB<DataModel>>(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') .watch$('serverInfo', 'statusInfo', 'currentBackup' as any, 'job')
.pipe(map(job => job || {})), .pipe(map(job => job || {})),
) )

View File

@@ -7,7 +7,7 @@ import {
Input, Input,
} from '@angular/core' } from '@angular/core'
import { Router } from '@angular/router' import { Router } from '@angular/router'
import { MarketplacePkg } from '@start9labs/marketplace' import { MarketplacePkgBase } from '@start9labs/marketplace'
import { import {
Exver, Exver,
ErrorService, ErrorService,
@@ -103,7 +103,7 @@ export class MarketplaceControlsComponent {
private readonly marketplaceService = inject(MarketplaceService) private readonly marketplaceService = inject(MarketplaceService)
@Input({ required: true }) @Input({ required: true })
pkg!: MarketplacePkg pkg!: MarketplacePkgBase
@Input() @Input()
localPkg!: PackageDataEntry | null localPkg!: PackageDataEntry | null

View File

@@ -22,10 +22,9 @@ import {
TuiButton, TuiButton,
TuiDialogContext, TuiDialogContext,
TuiDialogService, TuiDialogService,
TuiIcon,
TuiLoader, TuiLoader,
} from '@taiga-ui/core' } from '@taiga-ui/core'
import { TuiRadioList, TuiStringifyContentPipe } from '@taiga-ui/kit' import { TuiRadioList } from '@taiga-ui/kit'
import { import {
BehaviorSubject, BehaviorSubject,
combineLatest, combineLatest,
@@ -53,7 +52,7 @@ import { MarketplaceService } from 'src/app/services/marketplace.service'
@if (!(pkg.dependencyMetadata | empty)) { @if (!(pkg.dependencyMetadata | empty)) {
<marketplace-dependencies [pkg]="pkg" (open)="open($event)" /> <marketplace-dependencies [pkg]="pkg" (open)="open($event)" />
} }
<marketplace-additional [pkg]="pkg"> <marketplace-additional [pkg]="pkg" (static)="onStatic($event)">
@if (versions$ | async; as versions) { @if (versions$ | async; as versions) {
<marketplace-additional-item <marketplace-additional-item
(click)="versions.length ? selectVersion(pkg, version) : 0" (click)="versions.length ? selectVersion(pkg, version) : 0"
@@ -172,10 +171,8 @@ import { MarketplaceService } from 'src/app/services/marketplace.service'
AboutModule, AboutModule,
SharedPipesModule, SharedPipesModule,
FormsModule, FormsModule,
TuiStringifyContentPipe,
TuiRadioList, TuiRadioList,
TuiLoader, TuiLoader,
TuiIcon,
FlavorsComponent, FlavorsComponent,
], ],
}) })
@@ -228,6 +225,10 @@ export class MarketplacePreviewComponent {
this.router.navigate([], { queryParams: { id } }) this.router.navigate([], { queryParams: { id } })
} }
onStatic(type: 'License' | 'Instructions') {
// @TODO Alex need to display License or Instructions. This requires an API request, check out next/minor
}
selectVersion( selectVersion(
{ version }: MarketplacePkg, { version }: MarketplacePkg,
template: TemplateRef<TuiDialogContext>, template: TemplateRef<TuiDialogContext>,

View File

@@ -1,6 +1,6 @@
import { TUI_CONFIRM } from '@taiga-ui/kit' import { TUI_CONFIRM } from '@taiga-ui/kit'
import { inject, Injectable } from '@angular/core' import { inject, Injectable } from '@angular/core'
import { MarketplacePkg } from '@start9labs/marketplace' import { MarketplacePkg, MarketplacePkgBase } from '@start9labs/marketplace'
import { TuiDialogService } from '@taiga-ui/core' import { TuiDialogService } from '@taiga-ui/core'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { defaultIfEmpty, firstValueFrom } from 'rxjs' import { defaultIfEmpty, firstValueFrom } from 'rxjs'
@@ -60,7 +60,7 @@ export class MarketplaceAlertsService {
}) })
} }
async alertInstall({ alerts }: MarketplacePkg): Promise<boolean> { async alertInstall({ alerts }: MarketplacePkgBase): Promise<boolean> {
const content = alerts.install const content = alerts.install
return ( return (

View File

@@ -1,19 +1,27 @@
import { inject, Injectable } from '@angular/core' import { inject, Injectable } from '@angular/core'
import { import {
catchError,
defer, defer,
filter,
ignoreElements,
Observable, Observable,
repeat,
retry, retry,
shareReplay, shareReplay,
startWith, startWith,
switchMap, switchMap,
take,
tap,
} from 'rxjs' } from 'rxjs'
import { ServerMetrics } from 'src/app/services/api/api.types' import { ServerMetrics } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ConnectionService } from 'src/app/services/connection.service'
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class MetricsService extends Observable<ServerMetrics> { export class MetricsService extends Observable<ServerMetrics> {
private readonly connection = inject(ConnectionService)
private readonly api = inject(ApiService) private readonly api = inject(ApiService)
private readonly metrics$ = defer(() => private readonly metrics$ = defer(() =>
@@ -22,8 +30,10 @@ export class MetricsService extends Observable<ServerMetrics> {
switchMap(({ guid, metrics }) => switchMap(({ guid, metrics }) =>
this.api.openWebsocket$<ServerMetrics>(guid).pipe(startWith(metrics)), this.api.openWebsocket$<ServerMetrics>(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. catchError(() =>
retry(), this.connection.pipe(filter(Boolean), take(1), ignoreElements()),
),
repeat(),
shareReplay(1), shareReplay(1),
) )

View File

@@ -56,7 +56,7 @@ import { ChangeDetectionStrategy, Component, input } from '@angular/core'
:host { :host {
height: 100%; height: 100%;
min-height: 7.5rem; min-height: 9rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;

View File

@@ -16,11 +16,6 @@ import { TimeService } from 'src/app/services/time.service'
selector: 'metrics-time', selector: 'metrics-time',
template: ` template: `
@if (now(); as time) { @if (now(); as time) {
@if (!time.synced) {
<tui-notification appearance="warning">
<ng-container *ngTemplateOutlet="hint" />
</tui-notification>
}
<div tuiCell> <div tuiCell>
<div tuiTitle [style.text-align]="'center'"> <div tuiTitle [style.text-align]="'center'">
<div tuiSubtitle class="g-secondary"> <div tuiSubtitle class="g-secondary">
@@ -36,6 +31,11 @@ import { TimeService } from 'src/app/services/time.service'
/> />
} }
</div> </div>
@if (!time.synced) {
<tui-notification size="s" appearance="warning">
<ng-container *ngTemplateOutlet="hint" />
</tui-notification>
}
} @else { } @else {
Loading... Loading...
} }
@@ -61,10 +61,12 @@ import { TimeService } from 'src/app/services/time.service'
styles: ` styles: `
:host { :host {
height: 100%; height: 100%;
min-height: var(--tui-height-l);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
gap: 1rem; align-items: center;
gap: 0.5rem;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
[tuiCell], [tuiCell],
@@ -72,6 +74,10 @@ import { TimeService } from 'src/app/services/time.service'
[tuiSubtitle] { [tuiSubtitle] {
margin: 0; margin: 0;
justify-content: center; justify-content: center;
&::after {
display: none;
}
} }
} }
@@ -79,6 +85,10 @@ import { TimeService } from 'src/app/services/time.service'
display: none; display: none;
} }
tui-notification {
width: fit-content;
}
:host-context(tui-root._mobile) { :host-context(tui-root._mobile) {
tui-notification { tui-notification {
display: none; display: none;

View File

@@ -30,6 +30,7 @@ import { TimeService } from 'src/app/services/time.service'
styles: ` styles: `
:host { :host {
height: 100%; height: 100%;
min-height: var(--tui-height-l);
display: flex; display: flex;
text-align: center; text-align: center;
justify-content: center; justify-content: center;

View File

@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { T } from '@start9labs/start-sdk' import { T } from '@start9labs/start-sdk'
import { TuiIcon, TuiTitle } from '@taiga-ui/core' import { TuiTitle } from '@taiga-ui/core'
interface ActionItem { interface ActionItem {
readonly name: string readonly name: string
@@ -12,7 +12,6 @@ interface ActionItem {
@Component({ @Component({
selector: '[action]', selector: '[action]',
template: ` template: `
<tui-icon [icon]="action.icon || '@tui.circle-play'" />
<div tuiTitle> <div tuiTitle>
<strong>{{ action.name }}</strong> <strong>{{ action.name }}</strong>
<div tuiSubtitle>{{ action.description }}</div> <div tuiSubtitle>{{ action.description }}</div>
@@ -23,7 +22,7 @@ interface ActionItem {
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true, standalone: true,
imports: [TuiIcon, TuiTitle], imports: [TuiTitle],
host: { host: {
'[disabled]': '!!disabled', '[disabled]': '!!disabled',
}, },

View File

@@ -1,27 +1,27 @@
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
computed,
inject, inject,
Input, Input,
} from '@angular/core' } from '@angular/core'
import { T } from '@start9labs/start-sdk'
import { tuiPure } from '@taiga-ui/cdk'
import { TuiButton } from '@taiga-ui/core' 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 { 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 { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { PrimaryStatus } from 'src/app/services/pkg-status-rendering.service' import { PrimaryStatus } from 'src/app/services/pkg-status-rendering.service'
import { getManifest } from 'src/app/utils/get-package-data' import { getManifest } from 'src/app/utils/get-package-data'
@Component({ @Component({
selector: 'service-actions', selector: 'service-controls',
template: ` template: `
@if (['running', 'starting', 'restarting'].includes(status)) { @if (['running', 'starting', 'restarting'].includes(status)) {
<button <button
tuiButton tuiButton
appearance="secondary-destructive" appearance="secondary-destructive"
iconStart="@tui.square" iconStart="@tui.square"
(click)="actions.stop(manifest)" (click)="controls.stop(manifest())"
> >
Stop Stop
</button> </button>
@@ -31,7 +31,7 @@ import { getManifest } from 'src/app/utils/get-package-data'
<button <button
tuiButton tuiButton
iconStart="@tui.rotate-cw" iconStart="@tui.rotate-cw"
(click)="actions.restart(manifest)" (click)="controls.restart(manifest())"
> >
Restart Restart
</button> </button>
@@ -41,7 +41,7 @@ import { getManifest } from 'src/app/utils/get-package-data'
<button <button
tuiButton tuiButton
iconStart="@tui.play" iconStart="@tui.play"
(click)="actions.start(manifest, hasUnmet(dependencies))" (click)="controls.start(manifest(), !!hasUnmet)"
> >
Start Start
</button> </button>
@@ -78,24 +78,26 @@ import { getManifest } from 'src/app/utils/get-package-data'
standalone: true, standalone: true,
imports: [TuiButton], imports: [TuiButton],
}) })
export class ServiceActionsComponent { export class ServiceControlsComponent {
private readonly errors = inject(DepErrorService)
@Input({ required: true }) @Input({ required: true })
pkg!: PackageDataEntry pkg!: PackageDataEntry
@Input({ required: true }) @Input({ required: true })
status!: PrimaryStatus status!: PrimaryStatus
// TODO readonly manifest = computed(() => getManifest(this.pkg))
dependencies: readonly DependencyInfo[] = []
readonly actions = inject(ControlsService) readonly controls = inject(ControlsService)
get manifest(): T.Manifest { readonly hasUnmet = computed(() =>
return getManifest(this.pkg) this.errors.getPkgDepErrors$(this.manifest().id).pipe(
} map(errors =>
Object.keys(this.pkg.currentDependencies)
@tuiPure .map(id => errors[id])
hasUnmet(dependencies: readonly DependencyInfo[]): boolean { .some(Boolean),
return dependencies.some(dep => !!dep.errorText) ),
} ),
)
} }

View File

@@ -5,11 +5,11 @@ import {
Input, Input,
} from '@angular/core' } from '@angular/core'
import { RouterLink } from '@angular/router' import { RouterLink } from '@angular/router'
import { T } from '@start9labs/start-sdk'
import { TuiButton, TuiLink } from '@taiga-ui/core' import { TuiButton, TuiLink } from '@taiga-ui/core'
import { TuiBadge } from '@taiga-ui/kit' import { TuiBadge } from '@taiga-ui/kit'
import { ConfigService } from 'src/app/services/config.service' import { ConfigService } from 'src/app/services/config.service'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { MappedInterface } from '../types/mapped-interface'
@Component({ @Component({
selector: 'tr[serviceInterface]', selector: 'tr[serviceInterface]',
@@ -113,7 +113,10 @@ export class ServiceInterfaceComponent {
private readonly config = inject(ConfigService) private readonly config = inject(ConfigService)
@Input({ required: true }) @Input({ required: true })
info!: MappedInterface info!: T.ServiceInterface & {
public: boolean
routerLink: string
}
@Input({ required: true }) @Input({ required: true })
pkg!: PackageDataEntry pkg!: PackageDataEntry

View File

@@ -1,11 +1,12 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { T } from '@start9labs/start-sdk'
import { TuiLoader } from '@taiga-ui/core' 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 { InstallingInfo } from 'src/app/services/patch-db/data-model'
import { import {
PrimaryRendering, PrimaryRendering,
PrimaryStatus, PrimaryStatus,
} from 'src/app/services/pkg-status-rendering.service' } from 'src/app/services/pkg-status-rendering.service'
import { InstallingProgressDisplayPipe } from '../pipes/install-progress.pipe'
@Component({ @Component({
selector: 'service-status', selector: 'service-status',
@@ -17,7 +18,7 @@ import { InstallingProgressDisplayPipe } from '../pipes/install-progress.pipe'
<tui-loader size="s" [inheritColor]="true" /> <tui-loader size="s" [inheritColor]="true" />
Installing Installing
<span class="loading-dots"></span> <span class="loading-dots"></span>
{{ installingInfo.progress.overall | installingProgressString }} {{ getText(installingInfo.progress.overall) }}
</h3> </h3>
} @else { } @else {
<h3 [class]="class"> <h3 [class]="class">
@@ -84,7 +85,7 @@ import { InstallingProgressDisplayPipe } from '../pipes/install-progress.pipe'
host: { class: 'g-card' }, host: { class: 'g-card' },
standalone: true, standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [InstallingProgressDisplayPipe, TuiLoader], imports: [TuiLoader],
}) })
export class ServiceStatusComponent { export class ServiceStatusComponent {
@Input({ required: true }) @Input({ required: true })
@@ -120,4 +121,8 @@ export class ServiceStatusComponent {
get rendering() { get rendering() {
return PrimaryRendering[this.status] return PrimaryRendering[this.status]
} }
getText(progress: T.Progress): string {
return getProgressText(progress)
}
} }

View File

@@ -21,14 +21,7 @@ import { ServicesService } from './services.service'
<th tuiTh [requiredSort]="true" [sorter]="name">Name</th> <th tuiTh [requiredSort]="true" [sorter]="name">Name</th>
<th tuiTh>Version</th> <th tuiTh>Version</th>
<th tuiTh [requiredSort]="true" [sorter]="uptime">Uptime</th> <th tuiTh [requiredSort]="true" [sorter]="uptime">Uptime</th>
<th <th tuiTh [requiredSort]="true" [sorter]="status">Status</th>
tuiTh
[requiredSort]="true"
[sorter]="status"
[style.width.rem]="13"
>
Status
</th>
<th [style.width.rem]="8" [style.text-indent.rem]="1.5">Controls</th> <th [style.width.rem]="8" [style.text-indent.rem]="1.5">Controls</th>
</tr> </tr>
</thead> </thead>

View File

@@ -1,14 +1,9 @@
import { import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
ChangeDetectionStrategy,
Component,
inject,
Input,
} from '@angular/core'
import { tuiPure } from '@taiga-ui/cdk' import { tuiPure } from '@taiga-ui/cdk'
import { TuiIcon, TuiLoader } from '@taiga-ui/core' 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 { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service' import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
import { InstallingProgressDisplayPipe } from '../pipes/install-progress.pipe'
@Component({ @Component({
standalone: true, standalone: true,
@@ -31,6 +26,7 @@ import { InstallingProgressDisplayPipe } from '../pipes/install-progress.pipe'
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
height: 3rem; height: 3rem;
white-space: nowrap;
} }
:host-context(tui-root._mobile) { :host-context(tui-root._mobile) {
@@ -46,11 +42,8 @@ import { InstallingProgressDisplayPipe } from '../pipes/install-progress.pipe'
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TuiIcon, TuiLoader], imports: [TuiIcon, TuiLoader],
providers: [InstallingProgressDisplayPipe],
}) })
export class StatusComponent { export class StatusComponent {
private readonly pipe = inject(InstallingProgressDisplayPipe)
@Input() @Input()
pkg!: PackageDataEntry pkg!: PackageDataEntry
@@ -72,7 +65,7 @@ export class StatusComponent {
get status(): string { get status(): string {
if (this.pkg.stateInfo.installingInfo) { 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) { switch (this.getStatus(this.pkg).primary) {

View File

@@ -1,22 +1,6 @@
import { Pipe, PipeTransform } from '@angular/core' import { Pipe, PipeTransform } from '@angular/core'
import { T } from '@start9labs/start-sdk' 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({ @Pipe({
standalone: true, standalone: true,
name: 'installingProgress', name: 'installingProgress',
@@ -28,3 +12,12 @@ export class InstallingProgressPipe implements PipeTransform {
return Math.floor((100 * progress.done) / progress.total) 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'
}

View File

@@ -39,7 +39,7 @@ import {
section { section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
max-width: 32rem; max-width: 36rem;
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
} }
`, `,

View File

@@ -50,7 +50,7 @@ const OTHER = 'Other Custom Actions'
`, `,
styles: ` styles: `
section { section {
max-width: 54rem; max-width: 42rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin-bottom: 2rem; margin-bottom: 2rem;
@@ -74,18 +74,20 @@ export default class ServiceActionsRoute {
mainStatus: pkg.status.main, mainStatus: pkg.status.main,
icon: pkg.icon, icon: pkg.icon,
manifest: getManifest(pkg), manifest: getManifest(pkg),
actions: Object.keys(pkg.actions).reduce< actions: Object.entries(pkg.actions)
Record<string, ReadonlyArray<T.ActionMetadata & { id: string }>> .filter(([_, val]) => val.visibility !== 'hidden')
>( .reduce<
(acc, id) => { Record<string, ReadonlyArray<T.ActionMetadata & { id: string }>>
const action = { id, ...pkg.actions[id] } >(
const group = pkg.actions[id].group || OTHER (acc, [id]) => {
const current = acc[group] || [] const action = { id, ...pkg.actions[id] }
const group = pkg.actions[id].group || OTHER
const current = acc[group] || []
return { ...acc, [group]: current.concat(action) } return { ...acc, [group]: current.concat(action) }
}, },
{ [OTHER]: [] }, { [OTHER]: [] },
), ),
})), })),
), ),
) )
@@ -110,7 +112,7 @@ const REBUILD = {
icon: '@tui.wrench', icon: '@tui.wrench',
name: 'Rebuild Service', name: 'Rebuild Service',
description: 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 = { const UNINSTALL = {

View File

@@ -15,6 +15,7 @@ import { TuiBadge, TuiBreadcrumbs } from '@taiga-ui/kit'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component' import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
import { getAddresses } from 'src/app/routes/portal/components/interfaces/interface.utils' 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 { ConfigService } from 'src/app/services/config.service'
import { DataModel } from 'src/app/services/patch-db/data-model' import { DataModel } from 'src/app/services/patch-db/data-model'
import { TitleDirective } from 'src/app/services/title.service' import { TitleDirective } from 'src/app/services/title.service'
@@ -24,7 +25,7 @@ import { TitleDirective } from 'src/app/services/title.service'
<ng-container *title> <ng-container *title>
<a routerLink="../.." tuiIconButton iconStart="@tui.arrow-left">Back</a> <a routerLink="../.." tuiIconButton iconStart="@tui.arrow-left">Back</a>
{{ interface()?.name }} {{ interface()?.name }}
<ng-container *ngTemplateOutlet="badge" /> <interface-status [public]="!!interface()?.public" />
</ng-container> </ng-container>
<tui-breadcrumbs size="l" [style.margin-block-end.rem]="1"> <tui-breadcrumbs size="l" [style.margin-block-end.rem]="1">
<a *tuiItem tuiLink appearance="action-grayscale" routerLink="../.."> <a *tuiItem tuiLink appearance="action-grayscale" routerLink="../..">
@@ -32,7 +33,7 @@ import { TitleDirective } from 'src/app/services/title.service'
</a> </a>
<span *tuiItem class="g-primary"> <span *tuiItem class="g-primary">
{{ interface()?.name }} {{ interface()?.name }}
<ng-container *ngTemplateOutlet="badge" /> <interface-status [public]="!!interface()?.public" />
</span> </span>
</tui-breadcrumbs> </tui-breadcrumbs>
@if (interface(); as serviceInterface) { @if (interface(); as serviceInterface) {
@@ -41,16 +42,6 @@ import { TitleDirective } from 'src/app/services/title.service'
[serviceInterface]="serviceInterface" [serviceInterface]="serviceInterface"
/> />
} }
<ng-template #badge>
<tui-badge
[iconStart]="interface()?.public ? '@tui.globe' : '@tui.lock'"
[style.vertical-align.rem]="-0.125"
[style.margin]="'0 0.25rem -0.25rem'"
[appearance]="interface()?.public ? 'positive' : 'negative'"
>
{{ interface()?.public ? 'Public' : 'Private' }}
</tui-badge>
</ng-template>
`, `,
styles: ` styles: `
:host-context(tui-root._mobile) tui-breadcrumbs { :host-context(tui-root._mobile) tui-breadcrumbs {
@@ -70,6 +61,7 @@ import { TitleDirective } from 'src/app/services/title.service'
TuiLink, TuiLink,
TuiBadge, TuiBadge,
NgTemplateOutlet, NgTemplateOutlet,
InterfaceStatusComponent,
], ],
}) })
export default class ServiceInterfaceRoute { export default class ServiceInterfaceRoute {

View File

@@ -19,7 +19,7 @@ import {
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
import { getInstalledPrimaryStatus } from 'src/app/services/pkg-status-rendering.service' import { getInstalledPrimaryStatus } from 'src/app/services/pkg-status-rendering.service'
import { ServiceActionRequestsComponent } from '../components/action-requests.component' 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 { ServiceDependenciesComponent } from '../components/dependencies.component'
import { ServiceErrorComponent } from '../components/error.component' import { ServiceErrorComponent } from '../components/error.component'
import { ServiceHealthChecksComponent } from '../components/health-checks.component' import { ServiceHealthChecksComponent } from '../components/health-checks.component'
@@ -38,7 +38,7 @@ import { ServiceStatusComponent } from '../components/status.component'
<p class="g-secondary" [appUptime]="started"></p> <p class="g-secondary" [appUptime]="started"></p>
} }
@if (installed() && connected()) { @if (installed() && connected()) {
<service-actions [pkg]="pkg()" [status]="status()" /> <service-controls [pkg]="pkg()" [status]="status()" />
} }
</service-status> </service-status>
@@ -90,7 +90,7 @@ import { ServiceStatusComponent } from '../components/status.component'
CommonModule, CommonModule,
ServiceProgressComponent, ServiceProgressComponent,
ServiceStatusComponent, ServiceStatusComponent,
ServiceActionsComponent, ServiceControlsComponent,
ServiceInterfacesComponent, ServiceInterfacesComponent,
ServiceHealthChecksComponent, ServiceHealthChecksComponent,
ServiceDependenciesComponent, ServiceDependenciesComponent,

View File

@@ -1,9 +0,0 @@
export interface DependencyInfo {
id: string
title: string | null
icon: string | null
version: string
errorText: string
actionText: string
action: () => any
}

View File

@@ -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
}

View File

@@ -1,11 +1,11 @@
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { Component, inject, Input } from '@angular/core' import { Component, inject, Input } from '@angular/core'
import { Router, RouterLink } from '@angular/router'
import { import {
AboutModule, AboutModule,
AdditionalModule, AdditionalModule,
MarketplaceDependenciesComponent, MarketplaceDependenciesComponent,
MarketplacePackageHeroComponent, MarketplacePackageHeroComponent,
MarketplacePkgBase,
} from '@start9labs/marketplace' } from '@start9labs/marketplace'
import { import {
ErrorService, ErrorService,
@@ -13,66 +13,39 @@ import {
LoadingService, LoadingService,
SharedPipesModule, SharedPipesModule,
} from '@start9labs/shared' } 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 { 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 { DataModel } from 'src/app/services/patch-db/data-model'
import { getManifest } from 'src/app/utils/get-package-data' import { getManifest } from 'src/app/utils/get-package-data'
import { InstallingProgressPipe } from 'src/app/routes/portal/routes/services/pipes/install-progress.pipe' import { MarketplacePkgSideload } from './sideload.utils'
import { SideloadService } from './sideload.service'
@Component({ @Component({
selector: 'sideload-package', selector: 'sideload-package',
template: ` template: `
<div class="outer-container"> <div class="outer-container">
<ng-content /> <ng-content />
@if (progress$ | async; as progress) { <marketplace-package-hero [pkg]="pkg">
@for (phase of progress.phases; track $index) { <marketplace-controls
<p> slot="controls"
{{ phase.name }} class="controls-wrapper"
@if (phase.progress | installingProgress; as progress) { [pkg]="pkg"
: {{ progress }}% [localPkg]="local$ | async"
} [localFlavor]="!!(flavor$ | async)"
</p> />
<progress </marketplace-package-hero>
tuiProgressBar <div class="package-details">
size="xs" <div class="package-details-main">
[style.color]=" <marketplace-about [pkg]="pkg" />
phase.progress === true @if (!(pkg.dependencyMetadata | empty)) {
? 'var(--tui-text-positive)' <marketplace-dependencies [pkg]="pkg" />
: 'var(--tui-text-action)' }
" </div>
[attr.value]="(phase.progress | installingProgress) / 100 || null" <div class="package-details-additional">
></progress> <marketplace-additional [pkg]="pkg" (static)="onStatic($event)" />
} </div>
} @else { </div>
<marketplace-package-hero
*tuiLet="button$ | async as button"
[pkg]="package"
>
<div class="inner-container">
@if (button !== null && button !== 'Install') {
<a tuiButton [routerLink]="'/portal/services/' + package.id">
View installed
</a>
}
@if (button) {
<button tuiButton (click)="upload()">{{ button }}</button>
}
</div>
</marketplace-package-hero>
<!-- @TODO Matt do we want this here? How do we turn s9pk into MarketplacePkg? -->
<!-- <marketplace-about [pkg]="package" />-->
<!-- @if (!(package.dependencyMetadata | empty)) {-->
<!-- <marketplace-dependencies [pkg]="package" (open)="open($event)" />-->
<!-- }-->
<!-- <marketplace-additional [pkg]="package" />-->
}
</div> </div>
`, `,
styles: [ styles: [
@@ -83,95 +56,97 @@ import { SideloadService } from './sideload.service'
width: 100%; width: 100%;
@media (min-width: 1024px) { @media (min-width: 1024px) {
max-width: 80%;
margin: auto; margin: auto;
padding: 2.5rem 4rem 2rem 4rem; 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; display: flex;
justify-content: flex-start; justify-content: flex-start;
margin: -0.5rem 0 1.5rem -1px; gap: 0.5rem;
height: 4.5rem;
} }
`, `,
], ],
standalone: true, standalone: true,
imports: [ imports: [
CommonModule, CommonModule,
RouterLink,
SharedPipesModule, SharedPipesModule,
AboutModule, AboutModule,
AdditionalModule, AdditionalModule,
TuiButton,
TuiLet,
MarketplacePackageHeroComponent, MarketplacePackageHeroComponent,
MarketplaceDependenciesComponent, MarketplaceDependenciesComponent,
InstallingProgressPipe, MarketplaceControlsComponent,
TuiProgressBar,
], ],
}) })
export class SideloadPackageComponent { export class SideloadPackageComponent {
private readonly loader = inject(LoadingService) private readonly loader = inject(LoadingService)
private readonly api = inject(ApiService) private readonly api = inject(ApiService)
private readonly errorService = inject(ErrorService) private readonly errorService = inject(ErrorService)
private readonly router = inject(Router)
private readonly exver = inject(Exver) private readonly exver = inject(Exver)
private readonly sideloadService = inject(SideloadService) private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
readonly progress$ = this.sideloadService.progress$ // @Input({ required: true })
readonly button$ = combineLatest([ // pkg!: MarketplacePkgSideload
inject(ClientStorageService).showDevTools$,
inject<PatchDB<DataModel>>(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 ''
}
}),
)
// @Alex why do I need to initialize pkg below? I would prefer to do the above, but it's not working
@Input({ required: true }) @Input({ required: true })
package!: T.Manifest & { icon: string } pkg: MarketplacePkgSideload = {} as MarketplacePkgSideload
@Input({ required: true }) @Input({ required: true })
file!: File 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() { async upload() {
const loader = this.loader.open('Starting upload').subscribe() const loader = this.loader.open('Starting upload').subscribe()
try { try {
const { upload, progress } = await this.api.sideloadPackage() const { upload } = await this.api.sideloadPackage()
this.sideloadService.followProgress(progress)
this.api.uploadPackage(upload, this.file).catch(console.error) this.api.uploadPackage(upload, this.file).catch(console.error)
await firstValueFrom(this.progress$.pipe(filter(Boolean)))
} catch (e: any) { } catch (e: any) {
this.errorService.handleError(e) this.errorService.handleError(e)
} finally { } finally {
loader.unsubscribe() loader.unsubscribe()
} }
} }
open(id: string) {
this.router.navigate(['/marketplace'], { queryParams: { id } })
}
} }

View File

@@ -1,12 +1,11 @@
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef,
Component, Component,
inject, inject,
signal, signal,
} from '@angular/core' } from '@angular/core'
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'
import { T } from '@start9labs/start-sdk' import { MarketplacePkgBase } from '@start9labs/marketplace'
import { tuiIsString } from '@taiga-ui/cdk' import { tuiIsString } from '@taiga-ui/cdk'
import { TuiButton } from '@taiga-ui/core' import { TuiButton } from '@taiga-ui/core'
import { import {
@@ -17,16 +16,16 @@ import {
import { ConfigService } from 'src/app/services/config.service' import { ConfigService } from 'src/app/services/config.service'
import { TitleDirective } from 'src/app/services/title.service' import { TitleDirective } from 'src/app/services/title.service'
import { SideloadPackageComponent } from './package.component' import { SideloadPackageComponent } from './package.component'
import { parseS9pk } from './sideload.utils' import { MarketplacePkgSideload, validateS9pk } from './sideload.utils'
@Component({ @Component({
template: ` template: `
<ng-container *title>Sideload</ng-container> <ng-container *title>Sideload</ng-container>
@if (file && package()) { @if (file && package(); as pkg) {
<sideload-package [package]="package()!" [file]="file!"> <sideload-package [pkg]="pkg" [file]="file!">
<button <button
tuiIconButton tuiIconButton
appearance="secondary" appearance="neutral"
iconStart="@tui.x" iconStart="@tui.x"
[style.border-radius.%]="100" [style.border-radius.%]="100"
[style.justify-self]="'end'" [style.justify-self]="'end'"
@@ -55,7 +54,10 @@ import { parseS9pk } from './sideload.utils'
<tui-avatar appearance="secondary" src="@tui.cloud-upload" /> <tui-avatar appearance="secondary" src="@tui.cloud-upload" />
<p>Upload .s9pk package file</p> <p>Upload .s9pk package file</p>
@if (isTor) { @if (isTor) {
<p class="g-positive">Tip: switch to LAN for faster uploads</p> <p class="g-warning">
Warning: package upload will be slow over Tor. Switch to local
for a better experience.
</p>
} }
<button tuiButton>Upload</button> <button tuiButton>Upload</button>
</div> </div>
@@ -69,7 +71,7 @@ import { parseS9pk } from './sideload.utils'
` `
label { label {
height: 100%; height: 100%;
max-width: 40rem; max-width: 42rem;
margin: 0 auto; margin: 0 auto;
} }
@@ -91,11 +93,10 @@ import { parseS9pk } from './sideload.utils'
], ],
}) })
export default class SideloadComponent { export default class SideloadComponent {
private readonly cdr = inject(ChangeDetectorRef)
readonly isTor = inject(ConfigService).isTor() readonly isTor = inject(ConfigService).isTor()
file: File | null = null file: File | null = null
readonly package = signal<(T.Manifest & { icon: string }) | null>(null) readonly package = signal<MarketplacePkgSideload | null>(null)
readonly error = signal('') readonly error = signal('')
clear() { clear() {
@@ -105,12 +106,11 @@ export default class SideloadComponent {
} }
async onFile(file: File | null) { async onFile(file: File | null) {
const parsed = file ? await parseS9pk(file) : ''
this.file = file this.file = file
const parsed = file ? await validateS9pk(file) : ''
this.package.set(tuiIsString(parsed) ? null : parsed) this.package.set(tuiIsString(parsed) ? null : parsed)
this.error.set(tuiIsString(parsed) ? parsed : '') this.error.set(tuiIsString(parsed) ? parsed : '')
// @TODO Alex figure out why it is needed even though we use signals
this.cdr.markForCheck()
} }
} }

View File

@@ -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<string>()
private readonly errorService = inject(ErrorService)
private readonly router = inject(Router)
readonly progress$ = this.guid$.pipe(
switchMap(guid =>
this.api
.openWebsocket$<T.FullProgress>(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)
}
}

View File

@@ -1,31 +1,22 @@
import { S9pk, T } from '@start9labs/start-sdk' import { MarketplacePkgBase } from '@start9labs/marketplace'
import cbor from 'cbor' import { S9pk, ExtendedVersion } from '@start9labs/start-sdk'
const MAGIC = new Uint8Array([59, 59]) const MAGIC = new Uint8Array([59, 59])
const VERSION_1 = new Uint8Array([1]) const VERSION_1 = new Uint8Array([1])
const VERSION_2 = new Uint8Array([2]) const VERSION_2 = new Uint8Array([2])
interface Positions { export async function validateS9pk(
[key: string]: [bigint, bigint] // [position, length]
}
export async function parseS9pk(
file: File, file: File,
): Promise<(T.Manifest & { icon: string }) | string> { ): Promise<MarketplacePkgSideload | string> {
const magic = new Uint8Array(await blobToBuffer(file.slice(0, 2))) const magic = new Uint8Array(await blobToBuffer(file.slice(0, 2)))
const version = new Uint8Array(await blobToBuffer(file.slice(2, 3))) const version = new Uint8Array(await blobToBuffer(file.slice(2, 3)))
if (compare(magic, MAGIC)) { if (compare(magic, MAGIC)) {
try { try {
if (compare(version, VERSION_1)) { 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)) { } else if (compare(version, VERSION_2)) {
const s9pk = await S9pk.deserialize(file, null) return await parseS9pk(file)
return {
...s9pk.manifest,
icon: await s9pk.icon(),
}
} else { } else {
console.error(version) console.error(version)
@@ -43,92 +34,21 @@ export async function parseS9pk(
return 'Invalid package file' return 'Invalid package file'
} }
async function parseS9pkV1(file: File) { async function parseS9pk(file: File): Promise<MarketplacePkgSideload> {
const positions: Positions = {} const s9pk = await S9pk.deserialize(file, null)
// 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]),
),
)
return { return {
...(await cbor.decode(data, true)), ...s9pk.manifest,
icon: await blobToDataURL( dependencyMetadata: await s9pk.dependencyMetadata(),
file.slice( gitHash: '',
Number(positions['icon'][0]), icon: await s9pk.icon(),
Number(positions['icon'][0]) + Number(positions['icon'][1]), 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<string | ArrayBuffer | null> {
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<string> {
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<ArrayBuffer> { async function blobToBuffer(data: Blob | File): Promise<ArrayBuffer> {
const res = await readBlobToArrayBuffer(data) const res = await readBlobToArrayBuffer(data)
if (res instanceof String) { if (res instanceof String) {
@@ -158,3 +78,8 @@ async function readBlobToArrayBuffer(
function compare(a: Uint8Array, b: Uint8Array) { function compare(a: Uint8Array, b: Uint8Array) {
return a.length === b.length && a.every((value, index) => value === b[index]) return a.length === b.length && a.every((value, index) => value === b[index])
} }
export type MarketplacePkgSideload = MarketplacePkgBase & {
license: string
instructions: string
}

View File

@@ -93,7 +93,7 @@ import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
`, `,
styles: ` styles: `
:host { :host {
max-width: 40rem; max-width: 36rem;
} }
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -140,7 +140,7 @@ export default class SystemDomainsComponent {
this.formDialog.open(FormComponent, options) 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) { private getNetworkStrategy(strategy: any) {
return strategy.selection === 'local' return strategy.selection === 'local'
? { ipStrategy: strategy.value.ipStrategy } ? { ipStrategy: strategy.value.ipStrategy }
@@ -162,7 +162,7 @@ export default class SystemDomainsComponent {
loader.unsubscribe() 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<boolean> { private async claimDomain({ strategy }: any): Promise<boolean> {
const loader = this.loader.open('Saving...').subscribe() const loader = this.loader.open('Saving...').subscribe()
const networkStrategy = this.getNetworkStrategy(strategy) const networkStrategy = this.getNetworkStrategy(strategy)
@@ -177,7 +177,7 @@ export default class SystemDomainsComponent {
loader.unsubscribe() 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<boolean> { private async save({ provider, strategy, hostname }: any): Promise<boolean> {
const loader = this.loader.open('Saving...').subscribe() const loader = this.loader.open('Saving...').subscribe()
const name = provider.selection const name = provider.selection

View File

@@ -97,7 +97,7 @@ import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
`, `,
styles: ` styles: `
:host { :host {
max-width: 40rem; max-width: 36rem;
} }
form header, form header,

View File

@@ -8,6 +8,7 @@ import { PatchDB } from 'patch-db-client'
import { map } from 'rxjs' import { map } from 'rxjs'
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component' import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
import { getAddresses } from 'src/app/routes/portal/components/interfaces/interface.utils' 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 { ConfigService } from 'src/app/services/config.service'
import { DataModel } from 'src/app/services/patch-db/data-model' import { DataModel } from 'src/app/services/patch-db/data-model'
import { TitleDirective } from 'src/app/services/title.service' import { TitleDirective } from 'src/app/services/title.service'
@@ -34,10 +35,14 @@ const iface: T.ServiceInterface = {
<ng-container *title> <ng-container *title>
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">Back</a> <a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">Back</a>
StartOS UI StartOS UI
<interface-status [public]="!!ui()?.public" />
</ng-container> </ng-container>
<header tuiHeader> <header tuiHeader>
<hgroup tuiTitle> <hgroup tuiTitle>
<h3>{{ iface.name }}</h3> <h3>
{{ iface.name }}
<interface-status [public]="!!ui()?.public" />
</h3>
<p tuiSubtitle>{{ iface.description }}</p> <p tuiSubtitle>{{ iface.description }}</p>
</hgroup> </hgroup>
</header> </header>
@@ -54,6 +59,7 @@ const iface: T.ServiceInterface = {
TitleDirective, TitleDirective,
TuiHeader, TuiHeader,
TuiTitle, TuiTitle,
InterfaceStatusComponent,
], ],
}) })
export default class StartOsUiComponent { export default class StartOsUiComponent {

View File

@@ -62,7 +62,7 @@ export default class SystemProxiesComponent {
this.formDialog.open(FormComponent, options) 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<boolean> { private async save({ name, config }: any): Promise<boolean> {
const loader = this.loader.open('Saving...').subscribe() const loader = this.loader.open('Saving...').subscribe()

View File

@@ -1,38 +1,45 @@
import { Pipe, PipeTransform } from '@angular/core' import { Pipe, PipeTransform } from '@angular/core'
import { PlatformType } from 'src/app/services/api/api.types'
@Pipe({ @Pipe({
name: 'platformInfo', name: 'platformInfo',
standalone: true, standalone: true,
}) })
export class PlatformInfoPipe implements PipeTransform { export class PlatformInfoPipe implements PipeTransform {
transform(platforms: readonly PlatformType[]): { transform(userAgent: string | null): {
name: string name: string
icon: string icon: string
} { } {
const info = { if (!userAgent) {
name: '', return {
icon: '@tui.smartphone', name: 'CLI',
icon: '@tui.terminal',
}
} }
if (platforms.includes('cli')) { if (/Android/i.test(userAgent)) {
info.name = 'CLI' return {
info.icon = '@tui.terminal' name: 'Android Device',
} else if (platforms.includes('desktop')) { icon: '@tui.smartphone',
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'
} }
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',
}
} }
} }

View File

@@ -1,5 +1,4 @@
import { RouterLink } from '@angular/router' import { RouterLink } from '@angular/router'
import { TuiTable } from '@taiga-ui/addon-table'
import { TuiLet } from '@taiga-ui/cdk' import { TuiLet } from '@taiga-ui/cdk'
import { TuiButton, TuiTitle } from '@taiga-ui/core' import { TuiButton, TuiTitle } from '@taiga-ui/core'
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'

View File

@@ -33,10 +33,10 @@ import { PlatformInfoPipe } from './platform-info.pipe'
<span tuiFade class="agent">{{ session.userAgent }}</span> <span tuiFade class="agent">{{ session.userAgent }}</span>
</label> </label>
</td> </td>
@if (session.metadata.platforms | platformInfo; as info) { @if (session.userAgent | platformInfo; as platform) {
<td class="platform"> <td class="platform">
<tui-icon [icon]="info.icon" /> <tui-icon [icon]="platform.icon" />
{{ info.name }} {{ platform.name }}
</td> </td>
} }
<td class="date">{{ session.lastActive | date: 'medium' }}</td> <td class="date">{{ session.lastActive | date: 'medium' }}</td>

View File

@@ -103,7 +103,7 @@ import { wifiSpec } from './wifi.const'
`, `,
styles: ` styles: `
:host { :host {
max-width: 40rem; max-width: 36rem;
} }
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -935,26 +935,17 @@ export namespace Mock {
loggedIn: '2021-07-14T20:49:17.774Z', loggedIn: '2021-07-14T20:49:17.774Z',
lastActive: '2021-07-14T20:49:17.774Z', lastActive: '2021-07-14T20:49:17.774Z',
userAgent: 'AppleWebKit/{WebKit Rev} (KHTML, like Gecko)', userAgent: 'AppleWebKit/{WebKit Rev} (KHTML, like Gecko)',
metadata: {
platforms: ['iphone', 'mobileweb', 'mobile', 'ios'],
},
}, },
klndsfjhbwsajkdnaksj: { klndsfjhbwsajkdnaksj: {
loggedIn: '2021-07-14T20:49:17.774Z', loggedIn: '2021-07-14T20:49:17.774Z',
lastActive: '2019-07-14T20:49:17.774Z', lastActive: '2019-07-14T20:49:17.774Z',
userAgent: 'AppleWebKit/{WebKit Rev} (KHTML, like Gecko)', userAgent: 'AppleWebKit/{WebKit Rev} (KHTML, like Gecko)',
metadata: {
platforms: ['cli'],
},
}, },
b7b1a9cef4284f00af9e9dda6e676177: { b7b1a9cef4284f00af9e9dda6e676177: {
loggedIn: '2021-07-14T20:49:17.774Z', loggedIn: '2021-07-14T20:49:17.774Z',
lastActive: '2021-06-14T20:49:17.774Z', lastActive: '2021-06-14T20:49:17.774Z',
userAgent: userAgent:
'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0', '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({ spec: ISB.InputSpec.of({
/* TODO: Convert range for this value ([0, 2])*/
union: ISB.Value.union( union: ISB.Value.union(
{ {
name: 'Preference', name: 'Preference',
@@ -1560,19 +1550,18 @@ export namespace Mock {
}, },
disabled: ['option2'], disabled: ['option2'],
})), })),
'favorite-number': 'favorite-number': ISB.Value.number({
/* TODO: Convert range for this value ((-100,100])*/ ISB.Value.number( name: 'Favorite Number',
{ description: 'Your favorite number of all time',
name: 'Favorite Number', warning:
description: 'Your favorite number of all time', 'Once you set this number, it can never be changed without severe consequences.',
warning: required: false,
'Once you set this number, it can never be changed without severe consequences.', default: 7,
required: false, integer: false,
default: 7, units: 'BTC',
integer: false, min: -100,
units: 'BTC', max: 100,
}, }),
),
rpcsettings: ISB.Value.object( rpcsettings: ISB.Value.object(
{ {
name: 'RPC Settings', name: 'RPC Settings',
@@ -1906,7 +1895,7 @@ export namespace Mock {
name: 'View Properties', name: 'View Properties',
description: 'view important information about Bitcoin', description: 'view important information about Bitcoin',
warning: null, warning: null,
visibility: 'enabled', visibility: 'hidden',
allowedStatuses: 'any', allowedStatuses: 'any',
hasInput: false, hasInput: false,
group: null, group: null,

View File

@@ -31,7 +31,6 @@ export namespace RR {
export type LoginReq = { export type LoginReq = {
password: string password: string
metadata: SessionMetadata
ephemeral?: boolean ephemeral?: boolean
} // auth.login - unauthed } // auth.login - unauthed
export type loginRes = null export type loginRes = null
@@ -421,30 +420,8 @@ export type Session = {
loggedIn: string loggedIn: string
lastActive: string lastActive: string
userAgent: 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 type BackupTarget = DiskBackupTarget | CifsBackupTarget
export interface DiskBackupTarget { export interface DiskBackupTarget {
@@ -604,7 +581,7 @@ export type DependencyErrorTransitive = {
type: 'transitive' type: 'transitive'
} }
// **** @TODO 041 **** // @TODO 041
// export namespace RR041 { // export namespace RR041 {
// // ** domains ** // // ** domains **

View File

@@ -252,7 +252,7 @@ export const mockPatchData: DataModel = {
name: 'View Properties', name: 'View Properties',
description: 'view important information about Bitcoin', description: 'view important information about Bitcoin',
warning: null, warning: null,
visibility: 'enabled', visibility: 'hidden',
allowedStatuses: 'any', allowedStatuses: 'any',
hasInput: false, hasInput: false,
group: null, group: null,

View File

@@ -357,11 +357,11 @@ export function listUnique(spec: IST.ValueSpecList): ValidatorFn {
const objSpec = spec.spec const objSpec = spec.spec
let display1: string let display1: string
let display2: string let display2: string
let uniqueMessage = isObject(objSpec) let uniqueMessage = isListObject(objSpec)
? uniqueByMessageWrapper(objSpec.uniqueBy, objSpec) ? uniqueByMessageWrapper(objSpec.uniqueBy, objSpec)
: '' : ''
if (isObject(objSpec) && objSpec.displayAs) { if (isListObject(objSpec) && objSpec.displayAs) {
display1 = `"${(Mustache as any).render( display1 = `"${(Mustache as any).render(
objSpec.displayAs, objSpec.displayAs,
list[idx], list[idx],
@@ -390,7 +390,6 @@ function listItemEquals(
val1: any, val1: any,
val2: any, val2: any,
): boolean { ): boolean {
// TODO: fix types
switch (spec.spec.type) { switch (spec.spec.type) {
case 'text': case 'text':
return val1 == val2 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( function listObjEquals(
uniqueBy: IST.UniqueBy, uniqueBy: IST.UniqueBy,
spec: IST.ListValueSpecObject, spec: IST.ListValueSpecObject,
@@ -450,17 +410,17 @@ function listObjEquals(
if (!uniqueBy) { if (!uniqueBy) {
return false return false
} else if (typeof uniqueBy === 'string') { } 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) { } else if ('any' in uniqueBy) {
for (let subSpec of uniqueBy.any) { for (let unique of uniqueBy.any) {
if (listObjEquals(subSpec, spec, val1, val2)) { if (listObjEquals(unique, spec, val1, val2)) {
return true return true
} }
} }
return false return false
} else if ('all' in uniqueBy) { } else if ('all' in uniqueBy) {
for (let subSpec of uniqueBy.all) { for (let unique of uniqueBy.all) {
if (!listObjEquals(subSpec, spec, val1, val2)) { if (!listObjEquals(unique, spec, val1, val2)) {
return false return false
} }
} }
@@ -469,66 +429,29 @@ function listObjEquals(
return false return false
} }
function objEquals( function uniqueByEquals(spec: IST.ValueSpec, val1: any, val2: any): boolean {
uniqueBy: IST.UniqueBy, switch (spec.type) {
spec: IST.ValueSpecObject, case 'text':
val1: any, case 'textarea':
val2: any, case 'number':
): boolean { case 'toggle':
if (!uniqueBy) { case 'select':
return false case 'color':
} else if (typeof uniqueBy === 'string') { case 'datetime':
// TODO: fix types return val1 == val2
return itemEquals((spec as any)[uniqueBy], val1[uniqueBy], val2[uniqueBy]) case 'list':
} else if ('any' in uniqueBy) { if (val1.length !== val2.length) {
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)) {
return false return false
} }
} for (let idx = 0; idx < val1.length; idx++) {
return true if (listItemEquals(spec, val1[idx], val2[idx])) {
} return false
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
} }
} return true
return false default:
} else if ('all' in uniqueBy) { return false
for (let subSpec of uniqueBy.all) {
if (!unionEquals(subSpec, spec, val1, val2)) {
return false
}
}
return true
} }
return false
} }
function uniqueByMessageWrapper( function uniqueByMessageWrapper(
@@ -573,7 +496,7 @@ function uniqueByMessage(
: '(' + ret + ')' : '(' + ret + ')'
} }
function isObject( function isListObject(
spec: IST.ListValueSpecOf<any>, spec: IST.ListValueSpecOf<any>,
): spec is IST.ListValueSpecObject { ): spec is IST.ListValueSpecObject {
// only lists of objects have uniqueBy // only lists of objects have uniqueBy

View File

@@ -48,7 +48,6 @@ export class MarketplaceService {
this.registryUrl$.pipe( this.registryUrl$.pipe(
switchMap(url => this.fetchRegistry$(url)), switchMap(url => this.fetchRegistry$(url)),
filter(Boolean), filter(Boolean),
// @TODO is updateStoreName needed?
map(registry => { map(registry => {
registry.info.categories = { registry.info.categories = {
all: { all: {
@@ -217,7 +216,7 @@ export class MarketplaceService {
map(packages => { map(packages => {
return Object.entries(packages).flatMap(([id, pkgInfo]) => return Object.entries(packages).flatMap(([id, pkgInfo]) =>
Object.keys(pkgInfo.best).map(version => Object.keys(pkgInfo.best).map(version =>
this.convertToMarketplacePkg( this.convertRegistryPkgToMarketplacePkg(
id, id,
version, version,
this.exver.getFlavor(version), this.exver.getFlavor(version),
@@ -239,12 +238,12 @@ export class MarketplaceService {
this.api.getRegistryPackage(url, id, version ? `=${version}` : null), this.api.getRegistryPackage(url, id, version ? `=${version}` : null),
).pipe( ).pipe(
map(pkgInfo => map(pkgInfo =>
this.convertToMarketplacePkg(id, version, flavor, pkgInfo), this.convertRegistryPkgToMarketplacePkg(id, version, flavor, pkgInfo),
), ),
) )
} }
private convertToMarketplacePkg( private convertRegistryPkgToMarketplacePkg(
id: string, id: string,
version: string | null | undefined, version: string | null | undefined,
flavor: string | null, flavor: string | null,

View File

@@ -40,7 +40,7 @@ hr {
top left, top left,
top right; top right;
// TODO: Theme // @TODO Theme
background-color: color-mix( background-color: color-mix(
in hsl, in hsl,
var(--tui-background-base) 90%, var(--tui-background-base) 90%,

View File

@@ -22,7 +22,6 @@
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"paths": { "paths": {
/* These paths are relative to each app base folder */ /* These paths are relative to each app base folder */
/* @TODO Alex the marketplace path is different on 0351. verify */
"@start9labs/marketplace": ["../marketplace/index"], "@start9labs/marketplace": ["../marketplace/index"],
"@start9labs/shared": ["../shared/src/public-api"], "@start9labs/shared": ["../shared/src/public-api"],
"path": ["../../node_modules/path-browserify"] "path": ["../../node_modules/path-browserify"]