mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
Merge branch 'next/major' of github.com:Start9Labs/start-os into feature/nvidia
This commit is contained in:
2
Makefile
2
Makefile
@@ -278,7 +278,7 @@ ts-bindings: core/bindings/index.ts
|
||||
core/bindings/index.ts: $(call ls-files, core) $(ENVIRONMENT_FILE)
|
||||
rm -rf core/bindings
|
||||
./core/build/build-ts.sh
|
||||
ls core/bindings/*.ts | sed 's/core\/startos\/bindings\/\([^.]*\)\.ts/export { \1 } from ".\/\1";/g' | grep -v '"./index"' | tee core/bindings/index.ts
|
||||
ls core/bindings/*.ts | sed 's/core\/bindings\/\([^.]*\)\.ts/export { \1 } from ".\/\1";/g' | grep -v '"./index"' | tee core/bindings/index.ts
|
||||
npm --prefix sdk exec -- prettier --config ./sdk/base/package.json -w ./core/bindings/*.ts
|
||||
touch core/bindings/index.ts
|
||||
|
||||
|
||||
@@ -5,25 +5,24 @@ if [ -z "$VERSION" ]; then
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [ -z "$RUN_ID" ]; then
|
||||
>&2 echo '$RUN_ID required'
|
||||
exit 2
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
if [ "$SKIP_DL" != "1" ]; then
|
||||
rm -rf ~/Downloads/v$VERSION
|
||||
mkdir ~/Downloads/v$VERSION
|
||||
cd ~/Downloads/v$VERSION
|
||||
if [ "$SKIP_CLEAN" != "1" ]; then
|
||||
rm -rf ~/Downloads/v$VERSION
|
||||
mkdir ~/Downloads/v$VERSION
|
||||
cd ~/Downloads/v$VERSION
|
||||
fi
|
||||
|
||||
for arch in aarch64 aarch64-nonfree riscv64 x86_64 x86_64-nonfree raspberrypi; do
|
||||
while ! gh run download -R Start9Labs/start-os $RUN_ID -n $arch.squashfs -D $(pwd); do sleep 1; done
|
||||
done
|
||||
for arch in aarch64 aarch64-nonfree riscv64 x86_64 x86_64-nonfree; do
|
||||
while ! gh run download -R Start9Labs/start-os $RUN_ID -n $arch.iso -D $(pwd); do sleep 1; done
|
||||
done
|
||||
while ! gh run download -R Start9Labs/start-os $RUN_ID -n raspberrypi.img -D $(pwd); do sleep 1; done
|
||||
if [ -n "$RUN_ID" ]; then
|
||||
for arch in aarch64 aarch64-nonfree riscv64 x86_64 x86_64-nonfree raspberrypi; do
|
||||
while ! gh run download -R Start9Labs/start-os $RUN_ID -n $arch.squashfs -D $(pwd); do sleep 1; done
|
||||
done
|
||||
for arch in aarch64 aarch64-nonfree riscv64 x86_64 x86_64-nonfree; do
|
||||
while ! gh run download -R Start9Labs/start-os $RUN_ID -n $arch.iso -D $(pwd); do sleep 1; done
|
||||
done
|
||||
while ! gh run download -R Start9Labs/start-os $RUN_ID -n raspberrypi.img -D $(pwd); do sleep 1; done
|
||||
fi
|
||||
|
||||
if [ -n "$ST_RUN_ID" ]; then
|
||||
for arch in aarch64 riscv64 x86_64; do
|
||||
|
||||
2
container-runtime/package-lock.json
generated
2
container-runtime/package-lock.json
generated
@@ -38,7 +38,7 @@
|
||||
},
|
||||
"../sdk/dist": {
|
||||
"name": "@start9labs/start-sdk",
|
||||
"version": "0.4.0-beta.45",
|
||||
"version": "0.4.0-beta.46",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@iarna/toml": "^3.0.0",
|
||||
|
||||
@@ -10,7 +10,6 @@ import { SDKManifest } from "@start9labs/start-sdk/base/lib/types"
|
||||
import { SubContainerRc } from "@start9labs/start-sdk/package/lib/util/SubContainer"
|
||||
|
||||
const EMBASSY_HEALTH_INTERVAL = 15 * 1000
|
||||
const EMBASSY_PROPERTIES_LOOP = 30 * 1000
|
||||
/**
|
||||
* We wanted something to represent what the main loop is doing, and
|
||||
* in this case it used to run the properties, health, and the docker/ js main.
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
transformOldConfigToNew,
|
||||
} from "./transformConfigSpec"
|
||||
import { partialDiff } from "@start9labs/start-sdk/base/lib/util"
|
||||
import { Volume } from "@start9labs/start-sdk/package/lib/util/Volume"
|
||||
|
||||
type Optional<A> = A | undefined | null
|
||||
function todo(): never {
|
||||
@@ -61,14 +62,14 @@ export const EMBASSY_JS_LOCATION = "/usr/lib/startos/package/embassy.js"
|
||||
|
||||
const configFile = FileHelper.json(
|
||||
{
|
||||
volumeId: "embassy",
|
||||
base: new Volume("embassy"),
|
||||
subpath: "config.json",
|
||||
},
|
||||
matches.any,
|
||||
)
|
||||
const dependsOnFile = FileHelper.json(
|
||||
{
|
||||
volumeId: "embassy",
|
||||
base: new Volume("embassy"),
|
||||
subpath: "dependsOn.json",
|
||||
},
|
||||
dictionary([string, array(string)]),
|
||||
@@ -330,6 +331,10 @@ export class SystemForEmbassy implements System {
|
||||
) {
|
||||
this.version.upstream.prerelease = ["alpha"]
|
||||
}
|
||||
|
||||
if (this.manifest.id === "nostr") {
|
||||
this.manifest.id = "nostr-rs-relay"
|
||||
}
|
||||
}
|
||||
|
||||
async init(
|
||||
|
||||
2
core/.gitignore
vendored
2
core/.gitignore
vendored
@@ -8,4 +8,4 @@ secrets.db
|
||||
.env
|
||||
.editorconfig
|
||||
proptest-regressions/**/*
|
||||
/startos/bindings/*
|
||||
/bindings/*
|
||||
2
core/Cargo.lock
generated
2
core/Cargo.lock
generated
@@ -7664,7 +7664,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "start-os"
|
||||
version = "0.4.0-alpha.16"
|
||||
version = "0.4.0-alpha.17"
|
||||
dependencies = [
|
||||
"aes 0.7.5",
|
||||
"arti-client",
|
||||
|
||||
@@ -15,7 +15,7 @@ license = "MIT"
|
||||
name = "start-os"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/Start9Labs/start-os"
|
||||
version = "0.4.0-alpha.16" # VERSION_BUMP
|
||||
version = "0.4.0-alpha.17" # VERSION_BUMP
|
||||
|
||||
[lib]
|
||||
name = "startos"
|
||||
|
||||
@@ -94,7 +94,23 @@ impl Public {
|
||||
..Default::default()
|
||||
},
|
||||
gateways: OrdMap::new(),
|
||||
acme: BTreeMap::new(),
|
||||
acme: {
|
||||
let mut acme: BTreeMap<AcmeProvider, AcmeSettings> = Default::default();
|
||||
acme.insert(
|
||||
"letsencrypt".parse()?,
|
||||
AcmeSettings {
|
||||
contact: Vec::new(),
|
||||
},
|
||||
);
|
||||
#[cfg(feature = "dev")]
|
||||
acme.insert(
|
||||
"letsencrypt-staging".parse()?,
|
||||
AcmeSettings {
|
||||
contact: Vec::new(),
|
||||
},
|
||||
);
|
||||
acme
|
||||
},
|
||||
dns: Default::default(),
|
||||
},
|
||||
status_info: ServerStatus {
|
||||
|
||||
@@ -472,7 +472,7 @@ fn cert_send(cert: &X509, hostname: &Hostname) -> Result<Response, Error> {
|
||||
)
|
||||
.to_lowercase(),
|
||||
)
|
||||
.header(http::header::CONTENT_TYPE, "application/x-x509-ca-cert")
|
||||
.header(http::header::CONTENT_TYPE, "application/octet-stream")
|
||||
.header(http::header::CONTENT_LENGTH, pem.len())
|
||||
.header(
|
||||
http::header::CONTENT_DISPOSITION,
|
||||
|
||||
@@ -241,7 +241,7 @@ pub async fn mark_seen_before(
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
let n = db.as_private_mut().as_notifications_mut();
|
||||
for id in n.keys()?.range(..before) {
|
||||
for id in n.keys()?.range(..=before) {
|
||||
n.as_idx_mut(&id)
|
||||
.or_not_found(lazy_format!("Notification #{id}"))?
|
||||
.as_seen_mut()
|
||||
|
||||
@@ -176,7 +176,7 @@ impl S9pk<TmpSource<PackSource>> {
|
||||
|
||||
impl TryFrom<ManifestV1> for Manifest {
|
||||
type Error = Error;
|
||||
fn try_from(value: ManifestV1) -> Result<Self, Self::Error> {
|
||||
fn try_from(mut value: ManifestV1) -> Result<Self, Self::Error> {
|
||||
let default_url = value.upstream_repo.clone();
|
||||
let mut version = ExtendedVersion::from(
|
||||
exver::emver::Version::from_str(&value.version)
|
||||
@@ -190,6 +190,9 @@ impl TryFrom<ManifestV1> for Manifest {
|
||||
} else if &*value.id == "lightning-terminal" || &*value.id == "robosats" {
|
||||
version = version.map_upstream(|v| v.with_prerelease(["alpha".into()]));
|
||||
}
|
||||
if &*value.id == "nostr" {
|
||||
value.id = "nostr-rs-relay".parse()?;
|
||||
}
|
||||
Ok(Self {
|
||||
id: value.id,
|
||||
title: format!("{} (Legacy)", value.title).into(),
|
||||
|
||||
@@ -115,7 +115,7 @@ impl Manifest {
|
||||
if let Some(emulate_as) = &config.emulate_missing_as {
|
||||
expected.check_file(
|
||||
Path::new("images")
|
||||
.join(arch)
|
||||
.join(emulate_as)
|
||||
.join(image_id)
|
||||
.with_extension("squashfs"),
|
||||
)?;
|
||||
|
||||
@@ -24,6 +24,7 @@ impl FromStr for NamedHealthCheckResult {
|
||||
"success" => NamedHealthCheckResultKind::Success { message },
|
||||
"disabled" => NamedHealthCheckResultKind::Disabled { message },
|
||||
"starting" => NamedHealthCheckResultKind::Starting { message },
|
||||
"waiting" => NamedHealthCheckResultKind::Waiting { message },
|
||||
"loading" => NamedHealthCheckResultKind::Loading {
|
||||
message: message.unwrap_or_default(),
|
||||
},
|
||||
@@ -61,6 +62,7 @@ pub enum NamedHealthCheckResultKind {
|
||||
Success { message: Option<String> },
|
||||
Disabled { message: Option<String> },
|
||||
Starting { message: Option<String> },
|
||||
Waiting { message: Option<String> },
|
||||
Loading { message: String },
|
||||
Failure { message: String },
|
||||
}
|
||||
@@ -89,6 +91,13 @@ impl std::fmt::Display for NamedHealthCheckResult {
|
||||
write!(f, "{name}: Starting")
|
||||
}
|
||||
}
|
||||
NamedHealthCheckResultKind::Waiting { message } => {
|
||||
if let Some(message) = message {
|
||||
write!(f, "{name}: Waiting ({message})")
|
||||
} else {
|
||||
write!(f, "{name}: Waiting")
|
||||
}
|
||||
}
|
||||
NamedHealthCheckResultKind::Loading { message } => {
|
||||
write!(f, "{name}: Loading ({message})")
|
||||
}
|
||||
|
||||
@@ -56,8 +56,9 @@ mod v0_4_0_alpha_13;
|
||||
mod v0_4_0_alpha_14;
|
||||
mod v0_4_0_alpha_15;
|
||||
mod v0_4_0_alpha_16;
|
||||
mod v0_4_0_alpha_17;
|
||||
|
||||
pub type Current = v0_4_0_alpha_16::Version; // VERSION_BUMP
|
||||
pub type Current = v0_4_0_alpha_17::Version; // VERSION_BUMP
|
||||
|
||||
impl Current {
|
||||
#[instrument(skip(self, db))]
|
||||
@@ -175,7 +176,8 @@ enum Version {
|
||||
V0_4_0_alpha_13(Wrapper<v0_4_0_alpha_13::Version>),
|
||||
V0_4_0_alpha_14(Wrapper<v0_4_0_alpha_14::Version>),
|
||||
V0_4_0_alpha_15(Wrapper<v0_4_0_alpha_15::Version>),
|
||||
V0_4_0_alpha_16(Wrapper<v0_4_0_alpha_16::Version>), // VERSION_BUMP
|
||||
V0_4_0_alpha_16(Wrapper<v0_4_0_alpha_16::Version>),
|
||||
V0_4_0_alpha_17(Wrapper<v0_4_0_alpha_17::Version>), // VERSION_BUMP
|
||||
Other(exver::Version),
|
||||
}
|
||||
|
||||
@@ -234,7 +236,8 @@ impl Version {
|
||||
Self::V0_4_0_alpha_13(v) => DynVersion(Box::new(v.0)),
|
||||
Self::V0_4_0_alpha_14(v) => DynVersion(Box::new(v.0)),
|
||||
Self::V0_4_0_alpha_15(v) => DynVersion(Box::new(v.0)),
|
||||
Self::V0_4_0_alpha_16(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP
|
||||
Self::V0_4_0_alpha_16(v) => DynVersion(Box::new(v.0)),
|
||||
Self::V0_4_0_alpha_17(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP
|
||||
Self::Other(v) => {
|
||||
return Err(Error::new(
|
||||
eyre!("unknown version {v}"),
|
||||
@@ -285,7 +288,8 @@ impl Version {
|
||||
Version::V0_4_0_alpha_13(Wrapper(x)) => x.semver(),
|
||||
Version::V0_4_0_alpha_14(Wrapper(x)) => x.semver(),
|
||||
Version::V0_4_0_alpha_15(Wrapper(x)) => x.semver(),
|
||||
Version::V0_4_0_alpha_16(Wrapper(x)) => x.semver(), // VERSION_BUMP
|
||||
Version::V0_4_0_alpha_16(Wrapper(x)) => x.semver(),
|
||||
Version::V0_4_0_alpha_17(Wrapper(x)) => x.semver(), // VERSION_BUMP
|
||||
Version::Other(x) => x.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,6 +286,18 @@ impl VersionT for Version {
|
||||
ErrorKind::Filesystem,
|
||||
));
|
||||
}
|
||||
|
||||
if tokio::fs::metadata("/media/startos/data/package-data/volumes/nostr")
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
tokio::fs::rename(
|
||||
"/media/startos/data/package-data/volumes/nostr",
|
||||
"/media/startos/data/package-data/volumes/nostr-rs-relay",
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Should be the name of the package
|
||||
let mut paths = tokio::fs::read_dir(path).await?;
|
||||
while let Some(path) = paths.next_entry().await? {
|
||||
|
||||
53
core/src/version/v0_4_0_alpha_17.rs
Normal file
53
core/src/version/v0_4_0_alpha_17.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use exver::{PreReleaseSegment, VersionRange};
|
||||
|
||||
use super::v0_3_5::V0_3_0_COMPAT;
|
||||
use super::{VersionT, v0_4_0_alpha_16};
|
||||
use crate::db::model::public::AcmeSettings;
|
||||
use crate::net::acme::AcmeProvider;
|
||||
use crate::prelude::*;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref V0_4_0_alpha_17: exver::Version = exver::Version::new(
|
||||
[0, 4, 0],
|
||||
[PreReleaseSegment::String("alpha".into()), 17.into()]
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct Version;
|
||||
|
||||
impl VersionT for Version {
|
||||
type Previous = v0_4_0_alpha_16::Version;
|
||||
type PreUpRes = ();
|
||||
|
||||
async fn pre_up(self) -> Result<Self::PreUpRes, Error> {
|
||||
Ok(())
|
||||
}
|
||||
fn semver(self) -> exver::Version {
|
||||
V0_4_0_alpha_17.clone()
|
||||
}
|
||||
fn compat(self) -> &'static VersionRange {
|
||||
&V0_3_0_COMPAT
|
||||
}
|
||||
#[instrument(skip_all)]
|
||||
fn up(self, db: &mut Value, _: Self::PreUpRes) -> Result<Value, Error> {
|
||||
let acme = db["public"]["serverInfo"]["network"]["acme"]
|
||||
.as_object_mut()
|
||||
.or_not_found("public.serverInfo.network.acme")?;
|
||||
let letsencrypt =
|
||||
InternedString::intern::<&str>("letsencrypt".parse::<AcmeProvider>()?.as_ref());
|
||||
if !acme.contains_key(&letsencrypt) {
|
||||
acme.insert(
|
||||
letsencrypt,
|
||||
to_value(&AcmeSettings {
|
||||
contact: Vec::new(),
|
||||
})?,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Value::Null)
|
||||
}
|
||||
fn down(self, _db: &mut Value) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -224,8 +224,6 @@ export type ListValueSpecObject = {
|
||||
uniqueBy: UniqueBy
|
||||
displayAs: string | null
|
||||
}
|
||||
// TODO Aiden do we really want this expressivity? Why not the below. Also what's with the "readonly" portion?
|
||||
// export type UniqueBy = null | string | { any: string[] } | { all: string[] }
|
||||
|
||||
export type UniqueBy =
|
||||
| null
|
||||
|
||||
9
sdk/base/lib/osBindings/AddPackageSignerParams.ts
Normal file
9
sdk/base/lib/osBindings/AddPackageSignerParams.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Guid } from "./Guid"
|
||||
import type { PackageId } from "./PackageId"
|
||||
|
||||
export type AddPackageSignerParams = {
|
||||
id: PackageId
|
||||
signer: Guid
|
||||
versions: string | null
|
||||
}
|
||||
@@ -4,6 +4,7 @@ export type NamedHealthCheckResult = { name: string } & (
|
||||
| { result: "success"; message: string | null }
|
||||
| { result: "disabled"; message: string | null }
|
||||
| { result: "starting"; message: string | null }
|
||||
| { result: "waiting"; message: string | null }
|
||||
| { result: "loading"; message: string }
|
||||
| { result: "failure"; message: string }
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { PackageVersionInfo } from "./PackageVersionInfo"
|
||||
import type { Version } from "./Version"
|
||||
|
||||
export type PackageInfo = {
|
||||
authorized: Array<Guid>
|
||||
authorized: { [key: Guid]: string }
|
||||
versions: { [key: Version]: PackageVersionInfo }
|
||||
categories: string[]
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
import type { Guid } from "./Guid"
|
||||
import type { PackageId } from "./PackageId"
|
||||
|
||||
export type PackageSignerParams = { id: PackageId; signer: Guid }
|
||||
export type RemovePackageSignerParams = { id: PackageId; signer: Guid }
|
||||
@@ -5,6 +5,7 @@ export type SetHealth = { id: HealthCheckId; name: string } & (
|
||||
| { result: "success"; message: string | null }
|
||||
| { result: "disabled"; message: string | null }
|
||||
| { result: "starting"; message: string | null }
|
||||
| { result: "waiting"; message: string | null }
|
||||
| { result: "loading"; message: string }
|
||||
| { result: "failure"; message: string }
|
||||
)
|
||||
|
||||
@@ -14,6 +14,7 @@ export { AddAdminParams } from "./AddAdminParams"
|
||||
export { AddAssetParams } from "./AddAssetParams"
|
||||
export { AddCategoryParams } from "./AddCategoryParams"
|
||||
export { AddPackageParams } from "./AddPackageParams"
|
||||
export { AddPackageSignerParams } from "./AddPackageSignerParams"
|
||||
export { AddPackageToCategoryParams } from "./AddPackageToCategoryParams"
|
||||
export { AddressInfo } from "./AddressInfo"
|
||||
export { AddSslOptions } from "./AddSslOptions"
|
||||
@@ -154,7 +155,6 @@ export { PackageId } from "./PackageId"
|
||||
export { PackageIndex } from "./PackageIndex"
|
||||
export { PackageInfoShort } from "./PackageInfoShort"
|
||||
export { PackageInfo } from "./PackageInfo"
|
||||
export { PackageSignerParams } from "./PackageSignerParams"
|
||||
export { PackageState } from "./PackageState"
|
||||
export { PackageVersionInfo } from "./PackageVersionInfo"
|
||||
export { PasswordType } from "./PasswordType"
|
||||
@@ -172,6 +172,7 @@ export { RemoveAssetParams } from "./RemoveAssetParams"
|
||||
export { RemoveCategoryParams } from "./RemoveCategoryParams"
|
||||
export { RemovePackageFromCategoryParams } from "./RemovePackageFromCategoryParams"
|
||||
export { RemovePackageParams } from "./RemovePackageParams"
|
||||
export { RemovePackageSignerParams } from "./RemovePackageSignerParams"
|
||||
export { RemoveTunnelParams } from "./RemoveTunnelParams"
|
||||
export { RemoveVersionParams } from "./RemoveVersionParams"
|
||||
export { ReplayId } from "./ReplayId"
|
||||
|
||||
@@ -39,6 +39,7 @@ export class GetSystemSmtp {
|
||||
})
|
||||
await waitForNext
|
||||
}
|
||||
return new Promise<never>((_, rej) => rej(new Error("aborted")))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,7 +47,7 @@ export class GetSystemSmtp {
|
||||
*/
|
||||
watch(
|
||||
abort?: AbortSignal,
|
||||
): AsyncGenerator<T.SmtpValue | null, void, unknown> {
|
||||
): AsyncGenerator<T.SmtpValue | null, never, unknown> {
|
||||
const ctrl = new AbortController()
|
||||
abort?.addEventListener("abort", () => ctrl.abort())
|
||||
return DropGenerator.of(this.watchGen(ctrl.signal), () => ctrl.abort())
|
||||
|
||||
@@ -393,12 +393,13 @@ export class GetServiceInterface<Mapped = ServiceInterfaceFilled | null> {
|
||||
}
|
||||
await waitForNext
|
||||
}
|
||||
return new Promise<never>((_, rej) => rej(new Error("aborted")))
|
||||
}
|
||||
|
||||
/**
|
||||
* Watches the requested service interface. Returns an async iterator that yields whenever the value changes
|
||||
*/
|
||||
watch(abort?: AbortSignal): AsyncGenerator<Mapped, void, unknown> {
|
||||
watch(abort?: AbortSignal): AsyncGenerator<Mapped, never, unknown> {
|
||||
const ctrl = new AbortController()
|
||||
abort?.addEventListener("abort", () => ctrl.abort())
|
||||
return DropGenerator.of(this.watchGen(ctrl.signal), () => ctrl.abort())
|
||||
|
||||
@@ -2,11 +2,7 @@ import { Effects } from "../Effects"
|
||||
import { PackageId } from "../osBindings"
|
||||
import { deepEqual } from "./deepEqual"
|
||||
import { DropGenerator, DropPromise } from "./Drop"
|
||||
import {
|
||||
ServiceInterfaceFilled,
|
||||
filledAddress,
|
||||
getHostname,
|
||||
} from "./getServiceInterface"
|
||||
import { ServiceInterfaceFilled, filledAddress } from "./getServiceInterface"
|
||||
|
||||
const makeManyInterfaceFilled = async ({
|
||||
effects,
|
||||
@@ -106,12 +102,13 @@ export class GetServiceInterfaces<Mapped = ServiceInterfaceFilled[]> {
|
||||
}
|
||||
await waitForNext
|
||||
}
|
||||
return new Promise<never>((_, rej) => rej(new Error("aborted")))
|
||||
}
|
||||
|
||||
/**
|
||||
* Watches the service interfaces for the package. Returns an async iterator that yields whenever the value changes
|
||||
*/
|
||||
watch(abort?: AbortSignal): AsyncGenerator<Mapped, void, unknown> {
|
||||
watch(abort?: AbortSignal): AsyncGenerator<Mapped, never, unknown> {
|
||||
const ctrl = new AbortController()
|
||||
abort?.addEventListener("abort", () => ctrl.abort())
|
||||
return DropGenerator.of(this.watchGen(ctrl.signal), () => ctrl.abort())
|
||||
|
||||
@@ -65,8 +65,9 @@ import {
|
||||
ServiceInterfaceFilled,
|
||||
} from "../../base/lib/util/getServiceInterface"
|
||||
import { getOwnServiceInterfaces } from "../../base/lib/util/getServiceInterfaces"
|
||||
import { Volumes, createVolumes } from "./util/Volume"
|
||||
|
||||
export const OSVersion = testTypeVersion("0.4.0-alpha.16")
|
||||
export const OSVersion = testTypeVersion("0.4.0-alpha.17")
|
||||
|
||||
// prettier-ignore
|
||||
type AnyNeverCond<T extends any[], Then, Else> =
|
||||
@@ -132,6 +133,7 @@ export class StartSdk<Manifest extends T.SDKManifest> {
|
||||
|
||||
return {
|
||||
manifest: this.manifest,
|
||||
volumes: createVolumes(this.manifest),
|
||||
...startSdkEffectWrapper,
|
||||
setDataVersion,
|
||||
getDataVersion,
|
||||
@@ -430,10 +432,10 @@ export class StartSdk<Manifest extends T.SDKManifest> {
|
||||
query: Record<string, string>
|
||||
/** (optional) overrides the protocol prefix provided by the bind function.
|
||||
*
|
||||
* @example `ftp://`
|
||||
* @example `{ ssl: 'ftps', noSsl: 'ftp' }`
|
||||
*/
|
||||
schemeOverride: { ssl: Scheme; noSsl: Scheme } | null
|
||||
/** TODO Aiden how would someone include a password in the URL? Whether or not to mask the URLs on the screen, for example, when they contain a password */
|
||||
/** mask the url (recommended if it contains credentials such as an API key or password) */
|
||||
masked: boolean
|
||||
},
|
||||
) => new ServiceInterfaceBuilder({ ...options, effects }),
|
||||
|
||||
@@ -14,10 +14,7 @@ export { CommandController } from "./CommandController"
|
||||
import { EXIT_SUCCESS, HealthDaemon } from "./HealthDaemon"
|
||||
import { Daemon } from "./Daemon"
|
||||
import { CommandController } from "./CommandController"
|
||||
import { HealthCheck } from "../health/HealthCheck"
|
||||
import { Oneshot } from "./Oneshot"
|
||||
import { Manifest } from "../test/output.sdk"
|
||||
import { asError } from "../util"
|
||||
|
||||
export const cpExec = promisify(CP.exec)
|
||||
export const cpExecFile = promisify(CP.execFile)
|
||||
@@ -432,7 +429,7 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
|
||||
|
||||
async build() {
|
||||
for (const daemon of this.healthDaemons) {
|
||||
await daemon.init()
|
||||
await daemon.updateStatus()
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
@@ -3,10 +3,6 @@ import { defaultTrigger } from "../trigger/defaultTrigger"
|
||||
import { Ready } from "./Daemons"
|
||||
import { Daemon } from "./Daemon"
|
||||
import { SetHealth, Effects, SDKManifest } from "../../../base/lib/types"
|
||||
import { DEFAULT_SIGTERM_TIMEOUT } from "."
|
||||
import { asError } from "../../../base/lib/util/asError"
|
||||
import { Oneshot } from "./Oneshot"
|
||||
import { SubContainer } from "../util/SubContainer"
|
||||
|
||||
const oncePromise = <T>() => {
|
||||
let resolve: (value: T) => void
|
||||
@@ -26,7 +22,7 @@ export const EXIT_SUCCESS = "EXIT_SUCCESS" as const
|
||||
*
|
||||
*/
|
||||
export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
private _health: HealthCheckResult = { result: "starting", message: null }
|
||||
private _health: HealthCheckResult = { result: "waiting", message: null }
|
||||
private healthWatchers: Array<() => unknown> = []
|
||||
private running = false
|
||||
private started?: number
|
||||
@@ -82,20 +78,18 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
if (newStatus) {
|
||||
console.debug(`Launching ${this.id}...`)
|
||||
this.setupHealthCheck()
|
||||
;(await this.daemon)?.start()
|
||||
this.daemon?.start()
|
||||
this.started = performance.now()
|
||||
} else {
|
||||
console.debug(`Stopping ${this.id}...`)
|
||||
;(await this.daemon)?.term()
|
||||
this.turnOffHealthCheck()
|
||||
|
||||
this.setHealth({ result: "starting", message: null })
|
||||
this.daemon?.term()
|
||||
await this.turnOffHealthCheck()
|
||||
}
|
||||
}
|
||||
|
||||
private healthCheckCleanup: (() => null) | null = null
|
||||
private turnOffHealthCheck() {
|
||||
this.healthCheckCleanup?.()
|
||||
private healthCheckCleanup: (() => Promise<null>) | null = null
|
||||
private async turnOffHealthCheck() {
|
||||
await this.healthCheckCleanup?.()
|
||||
|
||||
this.resolvedReady = false
|
||||
this.readyPromise = new Promise(
|
||||
@@ -107,8 +101,7 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
)
|
||||
}
|
||||
private async setupHealthCheck() {
|
||||
const daemon = await this.daemon
|
||||
daemon?.onExit((success) => {
|
||||
this.daemon?.onExit((success) => {
|
||||
if (success && this.ready === "EXIT_SUCCESS") {
|
||||
this.setHealth({ result: "success", message: null })
|
||||
} else if (!success) {
|
||||
@@ -116,7 +109,7 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
result: "failure",
|
||||
message: `${this.id} daemon crashed`,
|
||||
})
|
||||
} else if (!daemon.isOneshot()) {
|
||||
} else if (!this.daemon?.isOneshot()) {
|
||||
this.setHealth({
|
||||
result: "failure",
|
||||
message: `${this.id} daemon exited`,
|
||||
@@ -132,6 +125,7 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
const { promise: status, resolve: setStatus } = oncePromise<{
|
||||
done: true
|
||||
}>()
|
||||
const { promise: exited, resolve: setExited } = oncePromise<null>()
|
||||
new Promise(async () => {
|
||||
if (this.ready === "EXIT_SUCCESS") return
|
||||
for (
|
||||
@@ -150,10 +144,12 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
|
||||
await this.setHealth(response)
|
||||
}
|
||||
setExited(null)
|
||||
}).catch((err) => console.error(`Daemon ${this.id} failed: ${err}`))
|
||||
|
||||
this.healthCheckCleanup = () => {
|
||||
this.healthCheckCleanup = async () => {
|
||||
setStatus({ done: true })
|
||||
await exited
|
||||
this.healthCheckCleanup = null
|
||||
return null
|
||||
}
|
||||
@@ -201,6 +197,7 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
const healths = this.dependencies.map((d) => ({
|
||||
health: d.running && d._health,
|
||||
id: d.id,
|
||||
display: typeof d.ready === "object" ? d.ready.display : null,
|
||||
}))
|
||||
const waitingOn = healths.filter(
|
||||
(h) => !h.health || h.health.result !== "success",
|
||||
@@ -209,18 +206,15 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
console.debug(
|
||||
`daemon ${this.id} waiting on ${waitingOn.map((w) => w.id)}`,
|
||||
)
|
||||
this.changeRunning(!waitingOn.length)
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (this.ready !== "EXIT_SUCCESS" && this.ready.display) {
|
||||
this.effects.setHealth({
|
||||
id: this.id,
|
||||
message: null,
|
||||
name: this.ready.display,
|
||||
result: "starting",
|
||||
})
|
||||
if (waitingOn.length) {
|
||||
const waitingOnNames = waitingOn.flatMap((w) =>
|
||||
w.display ? [w.display] : [],
|
||||
)
|
||||
const message = waitingOnNames.length ? waitingOnNames.join(", ") : null
|
||||
await this.setHealth({ result: "waiting", message })
|
||||
} else {
|
||||
await this.setHealth({ result: "starting", message: null })
|
||||
}
|
||||
await this.updateStatus()
|
||||
await this.changeRunning(!waitingOn.length)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ export class GetSslCertificate {
|
||||
})
|
||||
await waitForNext
|
||||
}
|
||||
return new Promise<never>((_, rej) => rej(new Error("aborted")))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,7 +58,7 @@ export class GetSslCertificate {
|
||||
*/
|
||||
watch(
|
||||
abort?: AbortSignal,
|
||||
): AsyncGenerator<[string, string, string], void, unknown> {
|
||||
): AsyncGenerator<[string, string, string], never, unknown> {
|
||||
const ctrl = new AbortController()
|
||||
abort?.addEventListener("abort", () => ctrl.abort())
|
||||
return DropGenerator.of(this.watchGen(ctrl.signal), () => ctrl.abort())
|
||||
|
||||
@@ -7,6 +7,7 @@ import { once } from "../../../base/lib/util/once"
|
||||
import { Drop } from "../../../base/lib/util/Drop"
|
||||
import { Mounts } from "../mainFn/Mounts"
|
||||
import { BackupEffects } from "../backup/Backups"
|
||||
import { PathBase } from "./Volume"
|
||||
|
||||
export const execFile = promisify(cp.execFile)
|
||||
const False = () => false
|
||||
@@ -71,10 +72,18 @@ async function bind(
|
||||
export interface SubContainer<
|
||||
Manifest extends T.SDKManifest,
|
||||
Effects extends T.Effects = T.Effects,
|
||||
> extends Drop {
|
||||
> extends Drop,
|
||||
PathBase {
|
||||
readonly imageId: keyof Manifest["images"] & T.ImageId
|
||||
readonly rootfs: string
|
||||
readonly guid: T.Guid
|
||||
|
||||
/**
|
||||
* Get the absolute path to a file or directory within this subcontainer's rootfs
|
||||
* @param path Path relative to the rootfs
|
||||
*/
|
||||
subpath(path: string): string
|
||||
|
||||
mount(
|
||||
mounts: Effects extends BackupEffects
|
||||
? Mounts<
|
||||
@@ -137,6 +146,22 @@ export interface SubContainer<
|
||||
options?: CommandOptions & StdioOptions,
|
||||
): Promise<cp.ChildProcess>
|
||||
|
||||
/**
|
||||
* @description Write a file to the subcontainer's filesystem
|
||||
* @param path Path relative to the subcontainer rootfs (e.g. "/etc/config.json")
|
||||
* @param data The data to write
|
||||
* @param options Optional write options (same as node:fs/promises writeFile)
|
||||
*/
|
||||
writeFile(
|
||||
path: string,
|
||||
data:
|
||||
| string
|
||||
| NodeJS.ArrayBufferView
|
||||
| Iterable<string | NodeJS.ArrayBufferView>
|
||||
| AsyncIterable<string | NodeJS.ArrayBufferView>,
|
||||
options?: Parameters<typeof fs.writeFile>[2],
|
||||
): Promise<void>
|
||||
|
||||
rc(): SubContainerRc<Manifest, Effects>
|
||||
|
||||
isOwned(): this is SubContainerOwned<Manifest, Effects>
|
||||
@@ -291,6 +316,12 @@ export class SubContainerOwned<
|
||||
}
|
||||
}
|
||||
|
||||
subpath(path: string): string {
|
||||
return path.startsWith("/")
|
||||
? `${this.rootfs}${path}`
|
||||
: `${this.rootfs}/${path}`
|
||||
}
|
||||
|
||||
async mount(
|
||||
mounts: Effects extends BackupEffects
|
||||
? Mounts<
|
||||
@@ -618,6 +649,27 @@ export class SubContainerOwned<
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Write a file to the subcontainer's filesystem
|
||||
* @param path Path relative to the subcontainer rootfs (e.g. "/etc/config.json")
|
||||
* @param data The data to write
|
||||
* @param options Optional write options (same as node:fs/promises writeFile)
|
||||
*/
|
||||
async writeFile(
|
||||
path: string,
|
||||
data:
|
||||
| string
|
||||
| NodeJS.ArrayBufferView
|
||||
| Iterable<string | NodeJS.ArrayBufferView>
|
||||
| AsyncIterable<string | NodeJS.ArrayBufferView>,
|
||||
options?: Parameters<typeof fs.writeFile>[2],
|
||||
): Promise<void> {
|
||||
const fullPath = this.subpath(path)
|
||||
const dir = fullPath.replace(/\/[^/]*\/?$/, "")
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
return fs.writeFile(fullPath, data, options)
|
||||
}
|
||||
|
||||
rc(): SubContainerRc<Manifest, Effects> {
|
||||
return new SubContainerRc(this)
|
||||
}
|
||||
@@ -643,6 +695,9 @@ export class SubContainerRc<
|
||||
get guid() {
|
||||
return this.subcontainer.guid
|
||||
}
|
||||
subpath(path: string): string {
|
||||
return this.subcontainer.subpath(path)
|
||||
}
|
||||
private destroyed = false
|
||||
private destroying: Promise<null> | null = null
|
||||
public constructor(
|
||||
@@ -800,6 +855,24 @@ export class SubContainerRc<
|
||||
return this.subcontainer.spawn(command, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Write a file to the subcontainer's filesystem
|
||||
* @param path Path relative to the subcontainer rootfs (e.g. "/etc/config.json")
|
||||
* @param data The data to write
|
||||
* @param options Optional write options (same as node:fs/promises writeFile)
|
||||
*/
|
||||
async writeFile(
|
||||
path: string,
|
||||
data:
|
||||
| string
|
||||
| NodeJS.ArrayBufferView
|
||||
| Iterable<string | NodeJS.ArrayBufferView>
|
||||
| AsyncIterable<string | NodeJS.ArrayBufferView>,
|
||||
options?: Parameters<typeof fs.writeFile>[2],
|
||||
): Promise<void> {
|
||||
return this.subcontainer.writeFile(path, data, options)
|
||||
}
|
||||
|
||||
rc(): SubContainerRc<Manifest, Effects> {
|
||||
return this.subcontainer.rc()
|
||||
}
|
||||
|
||||
88
sdk/package/lib/util/Volume.ts
Normal file
88
sdk/package/lib/util/Volume.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import * as fs from "node:fs/promises"
|
||||
import * as T from "../../../base/lib/types"
|
||||
|
||||
/**
|
||||
* Common interface for objects that have a subpath method (Volume, SubContainer, etc.)
|
||||
*/
|
||||
export interface PathBase {
|
||||
subpath(path: string): string
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Represents a volume in the StartOS filesystem.
|
||||
* Provides utilities for reading and writing files within the volume.
|
||||
*/
|
||||
export class Volume<Id extends string = string> implements PathBase {
|
||||
/**
|
||||
* The absolute path to this volume's root directory
|
||||
*/
|
||||
readonly path: string
|
||||
|
||||
constructor(readonly id: Id) {
|
||||
this.path = `/media/startos/volumes/${id}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the absolute path to a file or directory within this volume
|
||||
* @param subpath Path relative to the volume root
|
||||
*/
|
||||
subpath(subpath: string): string {
|
||||
return subpath.startsWith("/")
|
||||
? `${this.path}${subpath}`
|
||||
: `${this.path}/${subpath}`
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Read a file from this volume
|
||||
* @param subpath Path relative to the volume root (e.g. "config.json" or "/data/file.txt")
|
||||
* @param options Optional read options (same as node:fs/promises readFile)
|
||||
*/
|
||||
async readFile(
|
||||
subpath: string,
|
||||
options?: Parameters<typeof fs.readFile>[1],
|
||||
): Promise<Buffer | string> {
|
||||
const fullPath = this.subpath(subpath)
|
||||
return fs.readFile(fullPath, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Write a file to this volume
|
||||
* @param subpath Path relative to the volume root (e.g. "config.json" or "/data/file.txt")
|
||||
* @param data The data to write
|
||||
* @param options Optional write options (same as node:fs/promises writeFile)
|
||||
*/
|
||||
async writeFile(
|
||||
subpath: string,
|
||||
data:
|
||||
| string
|
||||
| NodeJS.ArrayBufferView
|
||||
| Iterable<string | NodeJS.ArrayBufferView>
|
||||
| AsyncIterable<string | NodeJS.ArrayBufferView>,
|
||||
options?: Parameters<typeof fs.writeFile>[2],
|
||||
): Promise<void> {
|
||||
const fullPath = this.subpath(subpath)
|
||||
const dir = fullPath.replace(/\/[^/]*\/?$/, "")
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
return fs.writeFile(fullPath, data, options)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-safe volumes object that provides Volume instances for each volume defined in the manifest
|
||||
*/
|
||||
export type Volumes<Manifest extends T.SDKManifest> = {
|
||||
[K in Manifest["volumes"][number]]: Volume<K>
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a type-safe volumes object from a manifest
|
||||
*/
|
||||
export function createVolumes<Manifest extends T.SDKManifest>(
|
||||
manifest: Manifest,
|
||||
): Volumes<Manifest> {
|
||||
const volumes = {} as Volumes<Manifest>
|
||||
for (const volumeId of manifest.volumes) {
|
||||
;(volumes as any)[volumeId] = new Volume(volumeId)
|
||||
}
|
||||
return volumes
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import * as T from "../../../base/lib/types"
|
||||
import * as fs from "node:fs/promises"
|
||||
import { asError, deepEqual } from "../../../base/lib/util"
|
||||
import { DropGenerator, DropPromise } from "../../../base/lib/util/Drop"
|
||||
import { PathBase } from "./Volume"
|
||||
|
||||
const previousPath = /(.+?)\/([^/]*)$/
|
||||
|
||||
@@ -88,11 +89,12 @@ export type Transformers<Raw = unknown, Transformed = unknown> = {
|
||||
onWrite: (value: Transformed) => Raw
|
||||
}
|
||||
|
||||
type ToPath = string | { volumeId: T.VolumeId; subpath: string }
|
||||
type ToPath = string | { base: PathBase; subpath: string }
|
||||
function toPath(path: ToPath): string {
|
||||
return typeof path === "string"
|
||||
? path
|
||||
: `/media/startos/volumes/${path.volumeId}/${path.subpath}`
|
||||
if (typeof path === "string") {
|
||||
return path
|
||||
}
|
||||
return path.base.subpath(path.subpath)
|
||||
}
|
||||
|
||||
type Validator<T, U> = matches.Validator<T, U> | matches.Validator<unknown, U>
|
||||
@@ -103,7 +105,7 @@ type ReadType<A> = {
|
||||
watch: (
|
||||
effects: T.Effects,
|
||||
abort?: AbortSignal,
|
||||
) => AsyncGenerator<A | null, null, unknown>
|
||||
) => AsyncGenerator<A | null, never, unknown>
|
||||
onChange: (
|
||||
effects: T.Effects,
|
||||
callback: (
|
||||
@@ -270,7 +272,7 @@ export class FileHelper<A> {
|
||||
await onCreated(this.path).catch((e) => console.error(asError(e)))
|
||||
}
|
||||
}
|
||||
return null
|
||||
return new Promise<never>((_, rej) => rej(new Error("aborted")))
|
||||
}
|
||||
|
||||
private readOnChange<B>(
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from "../../../base/lib/util"
|
||||
export { GetSslCertificate } from "./GetSslCertificate"
|
||||
|
||||
export { Drop } from "../../../base/lib/util/Drop"
|
||||
export { Volume, Volumes } from "./Volume"
|
||||
|
||||
4
sdk/package/package-lock.json
generated
4
sdk/package/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@start9labs/start-sdk",
|
||||
"version": "0.4.0-beta.45",
|
||||
"version": "0.4.0-beta.46",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@start9labs/start-sdk",
|
||||
"version": "0.4.0-beta.45",
|
||||
"version": "0.4.0-beta.46",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@iarna/toml": "^3.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@start9labs/start-sdk",
|
||||
"version": "0.4.0-beta.45",
|
||||
"version": "0.4.0-beta.46",
|
||||
"description": "Software development kit to facilitate packaging services for StartOS",
|
||||
"main": "./package/lib/index.js",
|
||||
"types": "./package/lib/index.d.ts",
|
||||
|
||||
1169
web/package-lock.json
generated
1169
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "startos-ui",
|
||||
"version": "0.4.0-alpha.16",
|
||||
"version": "0.4.0-alpha.17",
|
||||
"author": "Start9 Labs, Inc",
|
||||
"homepage": "https://start9.com/",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
"https://registry.start9.com/": "Start9 Registry",
|
||||
"https://community-registry.start9.com/": "Community Registry",
|
||||
"https://beta-registry.start9.com/": "Start9 Beta Registry",
|
||||
"https://community-beta-registry.start9.com/": "Community Beta Registry"
|
||||
"https://community-beta-registry.start9.com/": "Community Beta Registry",
|
||||
"https://alpha-registry-x.start9.com/": "Start9 Alpha Registry"
|
||||
},
|
||||
"startosRegistry": "https://beta-registry.start9.com/",
|
||||
"snakeHighScore": 0
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
i18nPipe,
|
||||
SharedPipesModule,
|
||||
} from '@start9labs/shared'
|
||||
import { TuiLet } from '@taiga-ui/cdk'
|
||||
import {
|
||||
TuiAppearance,
|
||||
TuiButton,
|
||||
@@ -29,7 +28,6 @@ import { MenuComponent } from './menu.component'
|
||||
TuiButton,
|
||||
CategoriesModule,
|
||||
StoreIconComponentModule,
|
||||
TuiLet,
|
||||
TuiAppearance,
|
||||
TuiIcon,
|
||||
TuiSkeleton,
|
||||
|
||||
@@ -2,18 +2,11 @@ import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { SharedPipesModule, TickerComponent } from '@start9labs/shared'
|
||||
import { TuiLet } from '@taiga-ui/cdk'
|
||||
import { ItemComponent } from './item.component'
|
||||
|
||||
@NgModule({
|
||||
declarations: [ItemComponent],
|
||||
exports: [ItemComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
SharedPipesModule,
|
||||
TickerComponent,
|
||||
TuiLet,
|
||||
],
|
||||
imports: [CommonModule, RouterModule, SharedPipesModule, TickerComponent],
|
||||
})
|
||||
export class ItemModule {}
|
||||
|
||||
@@ -146,7 +146,9 @@ export default class SuccessPage implements AfterViewInit {
|
||||
.getElementById('cert')
|
||||
?.setAttribute(
|
||||
'href',
|
||||
`data:application/x-x509-ca-cert;base64,${encodeURIComponent(this.cert!)}`,
|
||||
URL.createObjectURL(
|
||||
new Blob([this.cert!], { type: 'application/octet-stream' }),
|
||||
),
|
||||
)
|
||||
|
||||
const html = this.documentation?.nativeElement.innerHTML || ''
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AsyncPipe } from '@angular/common'
|
||||
import { Component, ElementRef, inject, input } from '@angular/core'
|
||||
import { Component, ElementRef, inject } from '@angular/core'
|
||||
import {
|
||||
INTERSECTION_ROOT,
|
||||
WA_INTERSECTION_ROOT,
|
||||
WaIntersectionObserver,
|
||||
} from '@ng-web-apis/intersection-observer'
|
||||
import { WaMutationObserver } from '@ng-web-apis/mutation-observer'
|
||||
@@ -36,12 +36,7 @@ import { SetupLogsService } from '../../services/setup-logs.service'
|
||||
NgDompurifyPipe,
|
||||
TuiScrollbar,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: INTERSECTION_ROOT,
|
||||
useExisting: ElementRef,
|
||||
},
|
||||
],
|
||||
providers: [{ provide: WA_INTERSECTION_ROOT, useExisting: ElementRef }],
|
||||
})
|
||||
export class LogsWindowComponent {
|
||||
readonly logs$ = inject(SetupLogsService)
|
||||
|
||||
@@ -40,7 +40,9 @@ import { i18nKey } from '../i18n/i18n.providers'
|
||||
class="button"
|
||||
[iconStart]="masked ? '@tui.eye' : '@tui.eye-off'"
|
||||
(click)="masked = !masked"
|
||||
></button>
|
||||
>
|
||||
{{ 'Reveal/Hide' | i18n }}
|
||||
</button>
|
||||
}
|
||||
</tui-textfield>
|
||||
<footer class="g-buttons">
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
import { Directive, inject, DOCUMENT } from '@angular/core'
|
||||
import { Directive, DOCUMENT, inject } from '@angular/core'
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
|
||||
import {
|
||||
MutationObserverService,
|
||||
provideMutationObserverInit,
|
||||
WaMutationObserverService,
|
||||
} from '@ng-web-apis/mutation-observer'
|
||||
import { tuiInjectElement } from '@taiga-ui/cdk'
|
||||
|
||||
@Directive({
|
||||
selector: '[safeLinks]',
|
||||
providers: [
|
||||
MutationObserverService,
|
||||
provideMutationObserverInit({
|
||||
childList: true,
|
||||
subtree: true,
|
||||
}),
|
||||
WaMutationObserverService,
|
||||
provideMutationObserverInit({ childList: true, subtree: true }),
|
||||
],
|
||||
})
|
||||
export class SafeLinksDirective {
|
||||
private readonly doc = inject(DOCUMENT)
|
||||
private readonly el = tuiInjectElement()
|
||||
private readonly sub = inject(MutationObserverService)
|
||||
private readonly sub = inject(WaMutationObserverService)
|
||||
.pipe(takeUntilDestroyed())
|
||||
.subscribe(() => {
|
||||
Array.from(this.doc.links)
|
||||
|
||||
@@ -90,6 +90,9 @@ export default {
|
||||
90: 'Root-CA ist vertrauenswürdig!',
|
||||
91: 'Installierte Dienste',
|
||||
92: 'Diagnosen für den Tor-Daemon auf diesem Server',
|
||||
93: 'Fingerabdruck kopieren',
|
||||
94: 'Warten',
|
||||
95: 'Warten auf',
|
||||
96: 'Öffentliche Domain hinzufügen',
|
||||
97: 'Wird entfernt',
|
||||
100: 'Nicht gespeicherte Änderungen',
|
||||
@@ -578,7 +581,7 @@ export default {
|
||||
611: 'Keine Service-Schnittstellen',
|
||||
612: 'Grund',
|
||||
613: 'Private Gateways für die StartOS-Benutzeroberfläche können nicht deaktiviert werden',
|
||||
614: 'CA-Fingerabdruck',
|
||||
614: 'Root-CA',
|
||||
615: 'DHCP-Server',
|
||||
616: 'DHCP-Server können nicht bearbeitet werden',
|
||||
617: 'Statisch',
|
||||
@@ -592,4 +595,5 @@ export default {
|
||||
625: 'Eine andere Version auswählen',
|
||||
626: 'Hochladen',
|
||||
627: 'UI öffnen',
|
||||
628: 'In Zwischenablage kopiert',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -89,6 +89,9 @@ export const ENGLISH = {
|
||||
'Root CA Trusted!': 90,
|
||||
'Installed services': 91, // as in, software services installed on this computer
|
||||
'Diagnostics for the Tor daemon on this server': 92,
|
||||
'Copy fingerprint': 93, // as in the fingerprint of a root certificate authority
|
||||
'Waiting': 94,
|
||||
'Waiting on': 95, // as in "awaiting"
|
||||
'Add public domain': 96,
|
||||
'Removing': 97,
|
||||
'Unsaved changes': 100,
|
||||
@@ -577,7 +580,7 @@ export const ENGLISH = {
|
||||
'No service interfaces': 611, // as in, there are no available interfaces (API, UI, etc) for this software application
|
||||
'Reason': 612, // as in, an explanation for something
|
||||
'Cannot disable private gateways for StartOS UI': 613,
|
||||
'CA fingerprint': 614, // as in, the unique, fixed-length digital identifier generated from a certificate's data using a cryptographic hash function
|
||||
'Root CA': 614, // as in, the unique, fixed-length digital identifier generated from a certificate's data using a cryptographic hash function
|
||||
'DHCP Servers': 615,
|
||||
'Cannot edit DHCP servers': 616,
|
||||
'Static': 617, // as in, unchanging
|
||||
@@ -591,4 +594,5 @@ export const ENGLISH = {
|
||||
'Select another version': 625,
|
||||
'Upload': 626, // as in, upload a file
|
||||
'Open UI': 627, // as in, upload a file
|
||||
'Copied to clipboard': 628,
|
||||
} as const
|
||||
|
||||
@@ -90,6 +90,9 @@ export default {
|
||||
90: '¡CA raíz confiable!',
|
||||
91: 'Servicios instalados',
|
||||
92: 'Diagnósticos para el demonio Tor en este servidor',
|
||||
93: 'Copiar huella digital',
|
||||
94: 'Esperando',
|
||||
95: 'En espera de',
|
||||
96: 'Agregar dominio público',
|
||||
97: 'Eliminando',
|
||||
100: 'Cambios no guardados',
|
||||
@@ -578,7 +581,7 @@ export default {
|
||||
611: 'Sin interfaces de servicio',
|
||||
612: 'Razón',
|
||||
613: 'No se pueden deshabilitar las puertas de enlace privadas para la interfaz de usuario de StartOS',
|
||||
614: 'Huella digital de la CA',
|
||||
614: 'CA raíz',
|
||||
615: 'Servidores DHCP',
|
||||
616: 'No se pueden editar los servidores DHCP',
|
||||
617: 'Estático',
|
||||
@@ -592,4 +595,5 @@ export default {
|
||||
625: 'Seleccionar otra versión',
|
||||
626: 'Subir',
|
||||
627: 'Abrir UI',
|
||||
628: 'Copiado al portapapeles',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -90,6 +90,9 @@ export default {
|
||||
90: 'Certificat racine approuvé !',
|
||||
91: 'Services installés',
|
||||
92: 'Diagnostics pour le service Tor sur ce serveur',
|
||||
93: 'Copier l’empreinte',
|
||||
94: 'En attente',
|
||||
95: 'En attente de',
|
||||
96: 'Ajouter un domaine public',
|
||||
97: 'Suppression',
|
||||
100: 'Modifications non enregistrées',
|
||||
@@ -578,7 +581,7 @@ export default {
|
||||
611: 'Aucune interface de service',
|
||||
612: 'Raison',
|
||||
613: "Impossible de désactiver les passerelles privées pour l'interface utilisateur StartOS",
|
||||
614: 'Empreinte de l’AC',
|
||||
614: 'CA racine',
|
||||
615: 'Serveurs DHCP',
|
||||
616: 'Impossible de modifier les serveurs DHCP',
|
||||
617: 'Statique',
|
||||
@@ -592,4 +595,5 @@ export default {
|
||||
625: 'Sélectionner une autre version',
|
||||
626: 'Téléverser',
|
||||
627: 'Ouvrir UI',
|
||||
628: 'Copié dans le presse-papiers',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -90,6 +90,9 @@ export default {
|
||||
90: 'Główny certyfikat CA zaufany!',
|
||||
91: 'Zainstalowane usługi',
|
||||
92: 'Diagnostyka demona Tor na tym serwerze',
|
||||
93: 'Kopiuj odcisk palca',
|
||||
94: 'Oczekiwanie',
|
||||
95: 'Oczekiwanie na',
|
||||
96: 'Dodaj domenę publiczną',
|
||||
97: 'Usuwanie',
|
||||
100: 'Niezapisane zmiany',
|
||||
@@ -578,7 +581,7 @@ export default {
|
||||
611: 'Brak interfejsów usług',
|
||||
612: 'Powód',
|
||||
613: 'Nie można wyłączyć prywatnych bram dla interfejsu użytkownika StartOS',
|
||||
614: 'Odcisk palca CA',
|
||||
614: 'głównego CA',
|
||||
615: 'Serwery DHCP',
|
||||
616: 'Nie można edytować serwerów DHCP',
|
||||
617: 'Statyczny',
|
||||
@@ -592,4 +595,5 @@ export default {
|
||||
625: 'Wybierz inną wersję',
|
||||
626: 'Prześlij',
|
||||
627: 'Otwórz UI',
|
||||
628: 'Skopiowano do schowka',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { forwardRef, signal } from '@angular/core'
|
||||
import { tuiCreateToken, tuiProvide } from '@taiga-ui/cdk'
|
||||
import { forwardRef, InjectionToken, signal } from '@angular/core'
|
||||
import { tuiProvide } from '@taiga-ui/cdk'
|
||||
import {
|
||||
TuiLanguageName,
|
||||
tuiLanguageSwitcher,
|
||||
@@ -11,12 +11,19 @@ import { i18nService } from './i18n.service'
|
||||
export type i18nKey = keyof typeof ENGLISH
|
||||
export type i18n = Record<(typeof ENGLISH)[i18nKey], string>
|
||||
|
||||
export const I18N = tuiCreateToken(signal<i18n | null>(null))
|
||||
export const I18N_LOADER =
|
||||
tuiCreateToken<(lang: TuiLanguageName) => Promise<i18n>>()
|
||||
export const I18N_STORAGE = tuiCreateToken<
|
||||
export const I18N = new InjectionToken('', {
|
||||
factory: () => signal<i18n | null>(null),
|
||||
})
|
||||
|
||||
export const I18N_LOADER = new InjectionToken<
|
||||
(lang: TuiLanguageName) => Promise<i18n>
|
||||
>('')
|
||||
|
||||
export const I18N_STORAGE = new InjectionToken<
|
||||
(lang: TuiLanguageName) => Promise<void>
|
||||
>(() => Promise.resolve())
|
||||
>('', {
|
||||
factory: () => () => Promise.resolve(),
|
||||
})
|
||||
|
||||
export const I18N_PROVIDERS = [
|
||||
tuiLanguageSwitcher(async (language: TuiLanguageName): Promise<unknown> => {
|
||||
|
||||
@@ -50,7 +50,6 @@ export * from './tokens/relative-url'
|
||||
|
||||
export * from './util/base-64'
|
||||
export * from './util/convert-ansi'
|
||||
export * from './util/copy-to-clipboard'
|
||||
export * from './util/format-progress'
|
||||
export * from './util/get-new-entries'
|
||||
export * from './util/get-pkg-id'
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { Clipboard } from '@angular/cdk/clipboard'
|
||||
import { TuiAlertService } from '@taiga-ui/core'
|
||||
import { copyToClipboard } from '../util/copy-to-clipboard'
|
||||
|
||||
import { i18nPipe } from '../i18n/i18n.pipe'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CopyService {
|
||||
private readonly clipboard = inject(Clipboard)
|
||||
private readonly i18n = inject(i18nPipe)
|
||||
private readonly alerts = inject(TuiAlertService)
|
||||
|
||||
async copy(text: string) {
|
||||
const success = await copyToClipboard(text)
|
||||
const success = this.clipboard.copy(text)
|
||||
const message = success ? 'Copied to clipboard' : 'Failed'
|
||||
const appearance = success ? 'positive' : 'negative'
|
||||
|
||||
this.alerts
|
||||
.open(success ? 'Copied to clipboard!' : 'Failed to copy to clipboard.')
|
||||
.subscribe()
|
||||
this.alerts.open(this.i18n.transform(message), { appearance }).subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ export class DownloadHTMLService {
|
||||
const elem = this.document.createElement('a')
|
||||
elem.setAttribute(
|
||||
'href',
|
||||
'data:text/plain;charset=utf-8,' + encodeURIComponent(html),
|
||||
URL.createObjectURL(
|
||||
new Blob([html], { type: 'application/octet-stream' }),
|
||||
),
|
||||
)
|
||||
elem.setAttribute('download', filename)
|
||||
elem.style.display = 'none'
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
HttpAngularOptions,
|
||||
HttpOptions,
|
||||
LocalHttpResponse,
|
||||
Method,
|
||||
} from '../types/http.types'
|
||||
import { RPCResponse, RPCOptions } from '../types/rpc.types'
|
||||
import { RELATIVE_URL } from '../tokens/relative-url'
|
||||
@@ -42,7 +41,7 @@ export class HttpService {
|
||||
const { method, headers, params, timeout } = opts
|
||||
|
||||
return this.httpRequest<RPCResponse<T>>({
|
||||
method: Method.POST,
|
||||
method: 'POST',
|
||||
url: fullUrl || this.relativeUrl,
|
||||
headers,
|
||||
body: { method, params },
|
||||
@@ -73,7 +72,7 @@ export class HttpService {
|
||||
}
|
||||
|
||||
let req: Observable<LocalHttpResponse<T>>
|
||||
if (method === Method.GET) {
|
||||
if (method === 'GET') {
|
||||
req = this.http.get(url, options as any) as any
|
||||
} else {
|
||||
req = this.http.post(url, body, options as any) as any
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { HttpHeaders, HttpResponse } from '@angular/common/http'
|
||||
|
||||
export enum Method {
|
||||
GET = 'GET',
|
||||
POST = 'POST',
|
||||
}
|
||||
export type Method = 'GET' | 'POST'
|
||||
|
||||
type ParamPrimitive = string | number | boolean
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
export async function copyToClipboard(str: string): Promise<boolean> {
|
||||
if (window.isSecureContext) {
|
||||
return navigator.clipboard
|
||||
.writeText(str)
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
}
|
||||
|
||||
const el = document.createElement('textarea')
|
||||
el.value = str
|
||||
el.setAttribute('readonly', '')
|
||||
el.style.position = 'absolute'
|
||||
el.style.left = '-9999px'
|
||||
document.body.appendChild(el)
|
||||
el.select()
|
||||
const didCopy = document.execCommand('copy')
|
||||
document.body.removeChild(el)
|
||||
return didCopy
|
||||
}
|
||||
@@ -12,8 +12,7 @@ import {
|
||||
ValidatorFn,
|
||||
Validators,
|
||||
} from '@angular/forms'
|
||||
import { DialogService, ErrorService } from '@start9labs/shared'
|
||||
import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile'
|
||||
import { ErrorService } from '@start9labs/shared'
|
||||
import { tuiMarkControlAsTouchedAndValidate, TuiValidator } from '@taiga-ui/cdk'
|
||||
import {
|
||||
TuiAlertService,
|
||||
|
||||
@@ -10,11 +10,11 @@ export class AuthService {
|
||||
private readonly storage = inject(WA_LOCAL_STORAGE)
|
||||
private readonly effect = effect(() => {
|
||||
if (this.authenticated()) {
|
||||
this.storage.setItem(KEY, JSON.stringify(true))
|
||||
this.storage?.setItem(KEY, JSON.stringify(true))
|
||||
} else {
|
||||
this.storage.removeItem(KEY)
|
||||
this.storage?.removeItem(KEY)
|
||||
}
|
||||
})
|
||||
|
||||
readonly authenticated = signal(Boolean(this.storage.getItem(KEY)))
|
||||
readonly authenticated = signal(Boolean(this.storage?.getItem(KEY)))
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
VERSION,
|
||||
WorkspaceConfig,
|
||||
} from '@start9labs/shared'
|
||||
import { tuiObfuscateOptionsProvider } from '@taiga-ui/cdk'
|
||||
import {
|
||||
TUI_DATE_FORMAT,
|
||||
TUI_DIALOGS_CLOSE,
|
||||
@@ -30,7 +31,7 @@ import {
|
||||
TUI_DATE_VALUE_TRANSFORMER,
|
||||
} from '@taiga-ui/kit'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { filter, of, pairwise } from 'rxjs'
|
||||
import { filter, identity, of, pairwise } from 'rxjs'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import {
|
||||
PATCH_CACHE,
|
||||
@@ -133,4 +134,10 @@ export const APP_PROVIDERS = [
|
||||
provide: VERSION,
|
||||
useFactory: () => inject(ConfigService).version,
|
||||
},
|
||||
tuiObfuscateOptionsProvider({
|
||||
recipes: {
|
||||
mask: ({ length }) => '•'.repeat(length),
|
||||
none: identity,
|
||||
},
|
||||
}),
|
||||
]
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { AsyncPipe } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
DestroyRef,
|
||||
forwardRef,
|
||||
HostBinding,
|
||||
inject,
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
@@ -15,17 +15,13 @@ import {
|
||||
} from '@angular/forms'
|
||||
import { DialogService, i18nKey, i18nPipe } from '@start9labs/shared'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { TuiAnimated } from '@taiga-ui/cdk'
|
||||
import {
|
||||
TUI_ANIMATIONS_SPEED,
|
||||
TuiButton,
|
||||
TuiError,
|
||||
tuiFadeIn,
|
||||
tuiHeightCollapse,
|
||||
TuiIcon,
|
||||
TuiLink,
|
||||
tuiParentStop,
|
||||
TuiTextfield,
|
||||
tuiToAnimationOptions,
|
||||
} from '@taiga-ui/core'
|
||||
import { TuiFieldErrorPipe, TuiTooltip } from '@taiga-ui/kit'
|
||||
import { filter } from 'rxjs'
|
||||
@@ -57,40 +53,40 @@ import { FormObjectComponent } from './object.component'
|
||||
</div>
|
||||
<tui-error [error]="order | tuiFieldError | async" />
|
||||
@for (item of array.control.controls; track item) {
|
||||
@if (spec.spec.type === 'object') {
|
||||
<form-object
|
||||
class="object"
|
||||
[class.object_open]="!!open.get(item)"
|
||||
[formGroup]="$any(item)"
|
||||
[spec]="$any(spec.spec)"
|
||||
[@tuiHeightCollapse]="animation"
|
||||
[@tuiFadeIn]="animation"
|
||||
[open]="!!open.get(item)"
|
||||
(openChange)="open.set(item, $event)"
|
||||
>
|
||||
{{ item.value | mustache: $any(spec.spec).displayAs }}
|
||||
<button
|
||||
tuiIconButton
|
||||
type="button"
|
||||
class="remove"
|
||||
iconStart="@tui.trash"
|
||||
appearance="icon"
|
||||
size="m"
|
||||
title="Remove"
|
||||
(click.stop)="removeAt($index)"
|
||||
></button>
|
||||
</form-object>
|
||||
} @else {
|
||||
<form-control
|
||||
class="control"
|
||||
tuiTextfieldSize="m"
|
||||
[formControl]="$any(item)"
|
||||
[spec]="$any(spec.spec)"
|
||||
[@tuiHeightCollapse]="animation"
|
||||
[@tuiFadeIn]="animation"
|
||||
(remove)="removeAt($index)"
|
||||
/>
|
||||
}
|
||||
<div tuiAnimated class="control">
|
||||
<div>
|
||||
@if (spec.spec.type === 'object') {
|
||||
<form-object
|
||||
class="object"
|
||||
[class.object_open]="!!open.get(item)"
|
||||
[formGroup]="$any(item)"
|
||||
[spec]="$any(spec.spec)"
|
||||
[open]="!!open.get(item)"
|
||||
(openChange)="open.set(item, $event)"
|
||||
>
|
||||
{{ item.value | mustache: $any(spec.spec).displayAs }}
|
||||
<button
|
||||
tuiIconButton
|
||||
type="button"
|
||||
class="remove"
|
||||
iconStart="@tui.trash"
|
||||
appearance="icon"
|
||||
size="m"
|
||||
title="Remove"
|
||||
(click.stop)="removeAt($index)"
|
||||
></button>
|
||||
</form-object>
|
||||
} @else {
|
||||
<form-control
|
||||
class="array"
|
||||
tuiTextfieldSize="m"
|
||||
[formControl]="$any(item)"
|
||||
[spec]="$any(spec.spec)"
|
||||
(remove)="removeAt($index)"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
`,
|
||||
styles: `
|
||||
@@ -145,11 +141,23 @@ import { FormObjectComponent } from './object.component'
|
||||
}
|
||||
|
||||
.control {
|
||||
display: block;
|
||||
margin: 0.5rem 0;
|
||||
display: grid;
|
||||
|
||||
form-control {
|
||||
display: block;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
> * {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&.tui-enter,
|
||||
&.tui-leave {
|
||||
animation-name: tuiFade, tuiCollapse;
|
||||
}
|
||||
}
|
||||
`,
|
||||
animations: [tuiFadeIn, tuiHeightCollapse, tuiParentStop],
|
||||
hostDirectives: [ControlDirective],
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
@@ -166,14 +174,13 @@ import { FormObjectComponent } from './object.component'
|
||||
MustachePipe,
|
||||
FormControlComponent,
|
||||
forwardRef(() => FormObjectComponent),
|
||||
TuiAnimated,
|
||||
],
|
||||
})
|
||||
export class FormArrayComponent {
|
||||
@Input({ required: true })
|
||||
spec!: IST.ValueSpecList
|
||||
|
||||
@HostBinding('@tuiParentStop')
|
||||
readonly animation = tuiToAnimationOptions(inject(TUI_ANIMATIONS_SPEED))
|
||||
readonly order = ERRORS
|
||||
readonly array = inject(FormArrayName)
|
||||
readonly open = new Map<AbstractControl, boolean>()
|
||||
@@ -181,6 +188,7 @@ export class FormArrayComponent {
|
||||
private warned = false
|
||||
private readonly formService = inject(FormService)
|
||||
private readonly destroyRef = inject(DestroyRef)
|
||||
private readonly cdr = inject(ChangeDetectorRef)
|
||||
private readonly dialog = inject(DialogService)
|
||||
|
||||
get canAdd(): boolean {
|
||||
@@ -226,5 +234,6 @@ export class FormArrayComponent {
|
||||
private addItem() {
|
||||
this.array.control.insert(0, this.formService.getListItem(this.spec))
|
||||
this.open.set(this.array.control.at(0), true)
|
||||
this.cdr.markForCheck()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,9 @@ import {
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { tuiPure, TuiValueChanges } from '@taiga-ui/cdk'
|
||||
import { TuiValueChanges } from '@taiga-ui/cdk'
|
||||
import { TuiElasticContainer } from '@taiga-ui/kit'
|
||||
import { FormService } from 'src/app/services/form.service'
|
||||
|
||||
import { FormControlComponent } from './control.component'
|
||||
import { FormGroupComponent } from './group.component'
|
||||
|
||||
@@ -73,7 +72,6 @@ export class FormUnionComponent implements OnChanges {
|
||||
}
|
||||
|
||||
// OTHER?
|
||||
@tuiPure
|
||||
onUnion(union: string) {
|
||||
this.spec.others = this.spec.others || {}
|
||||
this.spec.others[this.union] = this.form.control.controls['value']?.value
|
||||
@@ -84,9 +82,7 @@ export class FormUnionComponent implements OnChanges {
|
||||
[],
|
||||
this.spec.others[union],
|
||||
),
|
||||
{
|
||||
emitEvent: false,
|
||||
},
|
||||
{ emitEvent: false },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
TUI_LAST_DAY,
|
||||
TuiDay,
|
||||
TuiMapperPipe,
|
||||
tuiPure,
|
||||
TuiTime,
|
||||
} from '@taiga-ui/cdk'
|
||||
import { TuiIcon, TuiTextfield } from '@taiga-ui/core'
|
||||
@@ -44,7 +43,7 @@ import { HintPipe } from '../pipes/hint.pipe'
|
||||
[invalid]="control.invalid()"
|
||||
[readOnly]="readOnly"
|
||||
[disabled]="!!spec.disabled"
|
||||
[ngModel]="getTime(value)"
|
||||
[ngModel]="value | tuiMapper: getTime"
|
||||
(ngModelChange)="value = $event?.toString() || null"
|
||||
(blur)="control.onTouched()"
|
||||
/>
|
||||
@@ -128,7 +127,6 @@ export class FormDatetimeComponent extends Control<
|
||||
readonly min = TUI_FIRST_DAY
|
||||
readonly max = TUI_LAST_DAY
|
||||
|
||||
@tuiPure
|
||||
getTime(value: string | null) {
|
||||
return value ? TuiTime.fromString(value) : null
|
||||
}
|
||||
|
||||
@@ -4,15 +4,14 @@ import { invert } from '@start9labs/shared'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { tuiPure } from '@taiga-ui/cdk'
|
||||
import { TuiIcon, TuiTextfield } from '@taiga-ui/core'
|
||||
import { TuiMultiSelect, TuiTooltip } from '@taiga-ui/kit'
|
||||
|
||||
import { TuiChevron, TuiMultiSelect, TuiTooltip } from '@taiga-ui/kit'
|
||||
import { Control } from './control'
|
||||
import { HintPipe } from '../pipes/hint.pipe'
|
||||
|
||||
@Component({
|
||||
selector: 'form-multiselect',
|
||||
template: `
|
||||
<tui-textfield multi [disabledItemHandler]="disabledItemHandler">
|
||||
<tui-textfield multi tuiChevron [disabledItemHandler]="disabledItemHandler">
|
||||
@if (spec.name) {
|
||||
<label tuiLabel>{{ spec.name }}</label>
|
||||
}
|
||||
@@ -43,6 +42,7 @@ import { HintPipe } from '../pipes/hint.pipe'
|
||||
TuiIcon,
|
||||
TuiTooltip,
|
||||
HintPipe,
|
||||
TuiChevron,
|
||||
],
|
||||
})
|
||||
export class FormMultiselectComponent extends Control<
|
||||
|
||||
@@ -5,7 +5,7 @@ import { IST } from '@start9labs/start-sdk'
|
||||
import { TUI_IS_MOBILE } from '@taiga-ui/cdk'
|
||||
import { TuiDataList, TuiIcon, TuiTextfield } from '@taiga-ui/core'
|
||||
import {
|
||||
TuiDataListWrapper,
|
||||
TuiChevron,
|
||||
TuiFluidTypography,
|
||||
tuiFluidTypographyOptionsProvider,
|
||||
TuiSelect,
|
||||
@@ -19,6 +19,7 @@ import { HintPipe } from '../pipes/hint.pipe'
|
||||
selector: 'form-select',
|
||||
template: `
|
||||
<tui-textfield
|
||||
tuiChevron
|
||||
[tuiTextfieldCleaner]="false"
|
||||
[disabledItemHandler]="disabledItemHandler"
|
||||
(tuiActiveZoneChange)="!$event && control.onTouched()"
|
||||
@@ -76,6 +77,7 @@ import { HintPipe } from '../pipes/hint.pipe'
|
||||
TuiIcon,
|
||||
TuiTooltip,
|
||||
HintPipe,
|
||||
TuiChevron,
|
||||
],
|
||||
})
|
||||
export class FormSelectComponent extends Control<IST.ValueSpecSelect, string> {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import { IST, utils } from '@start9labs/start-sdk'
|
||||
import { tuiInjectElement } from '@taiga-ui/cdk'
|
||||
import { TuiButton, TuiIcon, TuiTextfield } from '@taiga-ui/core'
|
||||
@@ -53,7 +54,9 @@ import { HintPipe } from '../pipes/hint.pipe'
|
||||
size="xs"
|
||||
[iconStart]="masked ? '@tui.eye' : '@tui.eye-off'"
|
||||
(click)="masked = !masked"
|
||||
></button>
|
||||
>
|
||||
{{ 'Reveal/Hide' | i18n }}
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
tuiIconButton
|
||||
@@ -63,6 +66,7 @@ import { HintPipe } from '../pipes/hint.pipe'
|
||||
size="xs"
|
||||
title="Remove"
|
||||
class="remove"
|
||||
(pointerdown.prevent)="(0)"
|
||||
(click)="remove()"
|
||||
></button>
|
||||
@if (spec | hint; as hint) {
|
||||
@@ -76,7 +80,7 @@ import { HintPipe } from '../pipes/hint.pipe'
|
||||
order: 1;
|
||||
}
|
||||
|
||||
:host-context(form-array > form-control > :host) .remove {
|
||||
:host-context(form-control.array > :host) .remove {
|
||||
display: flex;
|
||||
}
|
||||
`,
|
||||
@@ -87,6 +91,7 @@ import { HintPipe } from '../pipes/hint.pipe'
|
||||
TuiIcon,
|
||||
TuiTooltip,
|
||||
HintPipe,
|
||||
i18nPipe,
|
||||
],
|
||||
})
|
||||
export class FormTextComponent extends Control<IST.ValueSpecText, string> {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { CopyService, i18nPipe } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiButton, TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiButton, TuiHint, TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiFade } from '@taiga-ui/kit'
|
||||
import { TuiCell } from '@taiga-ui/layout'
|
||||
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
@@ -35,14 +35,27 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
</div>
|
||||
<div tuiCell>
|
||||
<div tuiTitle>
|
||||
<strong>{{ 'CA fingerprint' | i18n }}</strong>
|
||||
<strong>{{ 'Root CA' | i18n }}</strong>
|
||||
<div tuiSubtitle tuiFade>{{ server.caFingerprint }}</div>
|
||||
</div>
|
||||
<a
|
||||
tuiIconButton
|
||||
download
|
||||
appearance="icon"
|
||||
iconStart="@tui.download"
|
||||
href="/static/local-root-ca.crt"
|
||||
[tuiHint]="'Download' | i18n"
|
||||
tuiHintDirection="bottom"
|
||||
>
|
||||
{{ 'Download' | i18n }}
|
||||
</a>
|
||||
<button
|
||||
tuiIconButton
|
||||
appearance="icon"
|
||||
iconStart="@tui.copy"
|
||||
(click)="copyService.copy(server.caFingerprint)"
|
||||
[tuiHint]="'Copy fingerprint' | i18n"
|
||||
tuiHintDirection="bottom"
|
||||
>
|
||||
{{ 'Copy' | i18n }}
|
||||
</button>
|
||||
@@ -65,7 +78,7 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
`,
|
||||
styles: '[tuiCell] { padding-inline: 0; white-space: nowrap }',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiTitle, TuiButton, TuiCell, i18nPipe, TuiFade],
|
||||
imports: [TuiTitle, TuiButton, TuiCell, i18nPipe, TuiFade, TuiHint],
|
||||
})
|
||||
export class AboutComponent {
|
||||
readonly copyService = inject(CopyService)
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { RouterLink, RouterLinkActive } from '@angular/router'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import {
|
||||
TUI_ANIMATIONS_SPEED,
|
||||
tuiFadeIn,
|
||||
TuiHint,
|
||||
TuiIcon,
|
||||
tuiScaleIn,
|
||||
tuiToAnimationOptions,
|
||||
tuiWidthCollapse,
|
||||
} from '@taiga-ui/core'
|
||||
import { TuiHint, TuiIcon } from '@taiga-ui/core'
|
||||
import { TuiBadgedContent, TuiBadgeNotification } from '@taiga-ui/kit'
|
||||
import { getMenu } from 'src/app/utils/system-utilities'
|
||||
|
||||
@@ -27,12 +19,7 @@ import { getMenu } from 'src/app/utils/system-utilities'
|
||||
[class.link_system]="item.routerLink === 'system'"
|
||||
[tuiHint]="rla.isActive ? '' : (item.name | i18n)"
|
||||
>
|
||||
<tui-badged-content
|
||||
[style.--tui-radius.%]="50"
|
||||
[@tuiFadeIn]="animation"
|
||||
[@tuiWidthCollapse]="animation"
|
||||
[@tuiScaleIn]="animation"
|
||||
>
|
||||
<tui-badged-content [style.--tui-radius.%]="50">
|
||||
@if (item.badge(); as badge) {
|
||||
<tui-badge-notification tuiSlot="top" size="s">
|
||||
{{ badge }}
|
||||
@@ -175,7 +162,6 @@ import { getMenu } from 'src/app/utils/system-utilities'
|
||||
display: none;
|
||||
}
|
||||
`,
|
||||
animations: [tuiFadeIn, tuiWidthCollapse, tuiScaleIn],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
TuiBadgeNotification,
|
||||
@@ -188,6 +174,5 @@ import { getMenu } from 'src/app/utils/system-utilities'
|
||||
],
|
||||
})
|
||||
export class HeaderNavigationComponent {
|
||||
readonly animation = tuiToAnimationOptions(inject(TUI_ANIMATIONS_SPEED))
|
||||
readonly utils = getMenu()
|
||||
}
|
||||
|
||||
@@ -3,36 +3,39 @@ import {
|
||||
Component,
|
||||
inject,
|
||||
input,
|
||||
output,
|
||||
signal,
|
||||
} from '@angular/core'
|
||||
import {
|
||||
CopyService,
|
||||
DialogService,
|
||||
i18nKey,
|
||||
i18nPipe,
|
||||
} from '@start9labs/shared'
|
||||
import { CopyService, DialogService, i18nPipe } from '@start9labs/shared'
|
||||
import { TUI_IS_MOBILE } from '@taiga-ui/cdk'
|
||||
import {
|
||||
TuiButton,
|
||||
tuiButtonOptionsProvider,
|
||||
TuiDataList,
|
||||
TuiDropdown,
|
||||
TuiIcon,
|
||||
TuiTextfield,
|
||||
} from '@taiga-ui/core'
|
||||
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { QRModal } from 'src/app/routes/portal/modals/qr.component'
|
||||
import { InterfaceComponent } from '../interface.component'
|
||||
|
||||
import { InterfaceAddressItemComponent } from './item.component'
|
||||
|
||||
@Component({
|
||||
selector: 'td[actions]',
|
||||
template: `
|
||||
<div class="desktop">
|
||||
<button tuiIconButton appearance="flat-grayscale" (click)="viewDetails()">
|
||||
{{ 'Address details' | i18n }}
|
||||
<tui-icon class="info" icon="@tui.info" background="@tui.info-filled" />
|
||||
</button>
|
||||
@if (interface.value()?.type === 'ui') {
|
||||
@if (interface.address().masked) {
|
||||
<button
|
||||
tuiIconButton
|
||||
appearance="flat-grayscale"
|
||||
[iconStart]="
|
||||
interface.currentlyMasked() ? '@tui.eye' : '@tui.eye-off'
|
||||
"
|
||||
(click)="interface.currentlyMasked.set(!interface.currentlyMasked())"
|
||||
>
|
||||
{{ 'Reveal/Hide' | i18n }}
|
||||
</button>
|
||||
}
|
||||
@if (interface.address().ui) {
|
||||
<a
|
||||
tuiIconButton
|
||||
appearance="flat-grayscale"
|
||||
@@ -67,15 +70,12 @@ import { InterfaceComponent } from '../interface.component'
|
||||
tuiIconButton
|
||||
appearance="flat-grayscale"
|
||||
iconStart="@tui.ellipsis-vertical"
|
||||
[tuiAppearanceState]="open ? 'hover' : null"
|
||||
[tuiAppearanceState]="open() ? 'hover' : null"
|
||||
[(tuiDropdownOpen)]="open"
|
||||
>
|
||||
{{ 'Actions' | i18n }}
|
||||
<tui-data-list *tuiTextfieldDropdown="let close">
|
||||
<button tuiOption new iconStart="@tui.info" (click)="viewDetails()">
|
||||
{{ 'Address details' | i18n }}
|
||||
</button>
|
||||
@if (interface.value()?.type === 'ui') {
|
||||
<tui-data-list *tuiTextfieldDropdown (click)="open.set(false)">
|
||||
@if (interface.address().ui) {
|
||||
<a
|
||||
tuiOption
|
||||
new
|
||||
@@ -87,6 +87,18 @@ import { InterfaceComponent } from '../interface.component'
|
||||
{{ 'Open' | i18n }}
|
||||
</a>
|
||||
}
|
||||
@if (interface.address().masked) {
|
||||
<button
|
||||
tuiOption
|
||||
new
|
||||
iconStart="@tui.eye"
|
||||
(click)="
|
||||
interface.currentlyMasked.set(!interface.currentlyMasked())
|
||||
"
|
||||
>
|
||||
{{ 'Reveal/Hide' | i18n }}
|
||||
</button>
|
||||
}
|
||||
<button tuiOption new iconStart="@tui.qr-code" (click)="showQR()">
|
||||
{{ 'Show QR' | i18n }}
|
||||
</button>
|
||||
@@ -94,7 +106,7 @@ import { InterfaceComponent } from '../interface.component'
|
||||
tuiOption
|
||||
new
|
||||
iconStart="@tui.copy"
|
||||
(click)="copyService.copy(href()); close()"
|
||||
(click)="copyService.copy(href())"
|
||||
>
|
||||
{{ 'Copy URL' | i18n }}
|
||||
</button>
|
||||
@@ -105,24 +117,12 @@ import { InterfaceComponent } from '../interface.component'
|
||||
styles: `
|
||||
:host {
|
||||
text-align: right;
|
||||
grid-area: 1 / 2 / 4 / 3;
|
||||
grid-area: 1/4/4/4;
|
||||
width: fit-content;
|
||||
place-content: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
:host-context(.uncommon-hidden) .desktop {
|
||||
height: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.info {
|
||||
background: var(--tui-status-info);
|
||||
|
||||
&::after {
|
||||
mask-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile {
|
||||
display: none;
|
||||
}
|
||||
@@ -136,15 +136,19 @@ import { InterfaceComponent } from '../interface.component'
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(tbody.uncommon-hidden) {
|
||||
.desktop {
|
||||
height: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.mobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`,
|
||||
imports: [
|
||||
TuiButton,
|
||||
TuiDropdown,
|
||||
TuiDataList,
|
||||
i18nPipe,
|
||||
TuiTextfield,
|
||||
TuiIcon,
|
||||
],
|
||||
imports: [TuiButton, TuiDropdown, TuiDataList, i18nPipe, TuiTextfield],
|
||||
providers: [tuiButtonOptionsProvider({ appearance: 'icon' })],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
@@ -152,27 +156,13 @@ export class AddressActionsComponent {
|
||||
readonly isMobile = inject(TUI_IS_MOBILE)
|
||||
readonly dialog = inject(DialogService)
|
||||
readonly copyService = inject(CopyService)
|
||||
readonly interface = inject(InterfaceComponent)
|
||||
readonly interface = inject(InterfaceAddressItemComponent)
|
||||
readonly open = signal(false)
|
||||
|
||||
readonly href = input.required<string>()
|
||||
readonly bullets = input.required<string[]>()
|
||||
readonly disabled = input.required<boolean>()
|
||||
|
||||
open = false
|
||||
|
||||
viewDetails() {
|
||||
this.dialog
|
||||
.openAlert(
|
||||
`<ul>${this.bullets()
|
||||
.map(b => `<li>${b}</li>`)
|
||||
.join('')}</ul>` as i18nKey,
|
||||
{
|
||||
label: 'About this address' as i18nKey,
|
||||
},
|
||||
)
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
showQR() {
|
||||
this.dialog
|
||||
.openComponent(new PolymorpheusComponent(QRModal), {
|
||||
|
||||
@@ -15,12 +15,13 @@ import { InterfaceAddressItemComponent } from './item.component'
|
||||
<header>{{ 'Addresses' | i18n }}</header>
|
||||
<tui-elastic-container>
|
||||
<table [appTable]="['Type', 'Access', 'Gateway', 'URL', null]">
|
||||
<th [style.width.rem]="2"></th>
|
||||
@for (address of addresses()?.common; track $index) {
|
||||
<tr [address]="address" [isRunning]="isRunning()"></tr>
|
||||
} @empty {
|
||||
@if (addresses()) {
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<td colspan="6">
|
||||
<app-placeholder icon="@tui.list-x">
|
||||
{{ 'No addresses' | i18n }}
|
||||
</app-placeholder>
|
||||
@@ -39,7 +40,7 @@ import { InterfaceAddressItemComponent } from './item.component'
|
||||
<tbody [class.uncommon-hidden]="!uncommon">
|
||||
@if (addresses()?.uncommon?.length && uncommon) {
|
||||
<tr [style.background]="'var(--tui-background-neutral-1)'">
|
||||
<td colspan="5"></td>
|
||||
<td colspan="6"></td>
|
||||
</tr>
|
||||
}
|
||||
@for (address of addresses()?.uncommon; track $index) {
|
||||
@@ -66,6 +67,20 @@ import { InterfaceAddressItemComponent } from './item.component'
|
||||
</tui-elastic-container>
|
||||
`,
|
||||
styles: `
|
||||
:host ::ng-deep {
|
||||
th:nth-child(2) {
|
||||
width: 5rem;
|
||||
}
|
||||
|
||||
th:nth-child(3) {
|
||||
width: 4rem;
|
||||
}
|
||||
|
||||
th:nth-child(4) {
|
||||
width: 17rem;
|
||||
}
|
||||
}
|
||||
|
||||
.g-table:has(caption) {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
@@ -76,6 +91,13 @@ import { InterfaceAddressItemComponent } from './item.component'
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
[tuiButton] {
|
||||
border-radius: var(--tui-radius-xs);
|
||||
margin-block-end: 0.75rem;
|
||||
}
|
||||
}
|
||||
`,
|
||||
host: { class: 'g-card' },
|
||||
imports: [
|
||||
|
||||
@@ -1,13 +1,38 @@
|
||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
signal,
|
||||
} from '@angular/core'
|
||||
import { DialogService, i18nKey, i18nPipe } from '@start9labs/shared'
|
||||
import { TuiObfuscatePipe } from '@taiga-ui/cdk'
|
||||
import { TuiButton, TuiIcon } from '@taiga-ui/core'
|
||||
import { TuiBadge } from '@taiga-ui/kit'
|
||||
import { DisplayAddress } from '../interface.service'
|
||||
import { AddressActionsComponent } from './actions.component'
|
||||
import { TuiBadge } from '@taiga-ui/kit'
|
||||
|
||||
@Component({
|
||||
selector: 'tr[address]',
|
||||
template: `
|
||||
@if (address(); as address) {
|
||||
<td [style.padding-inline-end]="0">
|
||||
<div class="wrapper">
|
||||
<button
|
||||
tuiIconButton
|
||||
appearance="flat-grayscale"
|
||||
(click)="viewDetails()"
|
||||
>
|
||||
{{ 'Address details' | i18n }}
|
||||
<tui-icon
|
||||
class="info"
|
||||
icon="@tui.info"
|
||||
background="@tui.info-filled"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="wrapper">{{ address.type }}</div>
|
||||
</td>
|
||||
@@ -26,13 +51,18 @@ import { TuiBadge } from '@taiga-ui/kit'
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
<td [style.order]="-1">
|
||||
<td [style.grid-area]="'1 / 1 / 1 / 3'">
|
||||
<div class="wrapper" [title]="address.gatewayName">
|
||||
{{ address.gatewayName || '-' }}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="wrapper" [title]="address.url">{{ address.url }}</div>
|
||||
<td [style.grid-area]="'3 / 1 / 3 / 3'">
|
||||
<div
|
||||
class="wrapper"
|
||||
[title]="address.masked && currentlyMasked() ? '' : address.url"
|
||||
>
|
||||
{{ address.url | tuiObfuscate: recipe() }}
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
actions
|
||||
@@ -46,20 +76,30 @@ import { TuiBadge } from '@taiga-ui/kit'
|
||||
styles: `
|
||||
:host {
|
||||
white-space: nowrap;
|
||||
grid-template-columns: fit-content(10rem) 1fr 2rem 2rem;
|
||||
|
||||
td:last-child {
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
background: var(--tui-status-info);
|
||||
|
||||
&::after {
|
||||
mask-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.uncommon-hidden) {
|
||||
.wrapper {
|
||||
height: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
td {
|
||||
padding-block: 0;
|
||||
td,
|
||||
& {
|
||||
padding-block: 0 !important;
|
||||
border: hidden;
|
||||
}
|
||||
}
|
||||
@@ -76,23 +116,50 @@ import { TuiBadge } from '@taiga-ui/kit'
|
||||
:host-context(tui-root._mobile) {
|
||||
td {
|
||||
width: auto !important;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
display: none;
|
||||
grid-area: 1 / 3 / 4 / 3;
|
||||
}
|
||||
|
||||
td:nth-child(2) {
|
||||
font: var(--tui-font-text-m);
|
||||
font-weight: bold;
|
||||
color: var(--tui-text-primary);
|
||||
padding-inline-end: 0.5rem;
|
||||
}
|
||||
}
|
||||
`,
|
||||
imports: [i18nPipe, AddressActionsComponent, TuiBadge],
|
||||
imports: [
|
||||
i18nPipe,
|
||||
AddressActionsComponent,
|
||||
TuiBadge,
|
||||
TuiObfuscatePipe,
|
||||
TuiButton,
|
||||
TuiIcon,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class InterfaceAddressItemComponent {
|
||||
private readonly dialogs = inject(DialogService)
|
||||
|
||||
readonly address = input.required<DisplayAddress>()
|
||||
readonly isRunning = input.required<boolean>()
|
||||
|
||||
readonly currentlyMasked = signal(true)
|
||||
readonly recipe = computed(() =>
|
||||
this.address()?.masked && this.currentlyMasked() ? 'mask' : 'none',
|
||||
)
|
||||
|
||||
viewDetails() {
|
||||
this.dialogs
|
||||
.openAlert(
|
||||
`<ul>${this.address()
|
||||
.bullets.map(b => `<li>${b}</li>`)
|
||||
.join('')}</ul>` as i18nKey,
|
||||
{ label: 'About this address' as i18nKey },
|
||||
)
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,10 @@ import { InterfaceAddressesComponent } from './addresses/addresses.component'
|
||||
template: `
|
||||
<div>
|
||||
<section [gateways]="value()?.gateways"></section>
|
||||
<section [publicDomains]="value()?.publicDomains"></section>
|
||||
<section
|
||||
[publicDomains]="value()?.publicDomains"
|
||||
[addSsl]="value()?.addSsl || false"
|
||||
></section>
|
||||
<section [torDomains]="value()?.torDomains"></section>
|
||||
<section [privateDomains]="value()?.privateDomains"></section>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,8 @@ type AddressWithInfo = {
|
||||
info: T.HostnameInfo
|
||||
gateway?: GatewayPlus
|
||||
showSsl: boolean
|
||||
masked: boolean
|
||||
ui: boolean
|
||||
}
|
||||
|
||||
function cmpWithRankedPredicates<T extends AddressWithInfo>(
|
||||
@@ -130,6 +132,9 @@ export class InterfaceService {
|
||||
|
||||
if (!hostnamesInfos.length) return addresses
|
||||
|
||||
const masked = serviceInterface.masked
|
||||
const ui = serviceInterface.type === 'ui'
|
||||
|
||||
const allAddressesWithInfo: AddressWithInfo[] = hostnamesInfos.flatMap(
|
||||
h => {
|
||||
const { url, sslUrl } = utils.addressHostToUrl(
|
||||
@@ -143,7 +148,14 @@ export class InterfaceService {
|
||||
: undefined
|
||||
const res = []
|
||||
if (url) {
|
||||
res.push({ url, info, gateway, showSsl: false })
|
||||
res.push({
|
||||
url,
|
||||
info,
|
||||
gateway,
|
||||
showSsl: false,
|
||||
masked,
|
||||
ui,
|
||||
})
|
||||
}
|
||||
if (sslUrl) {
|
||||
res.push({
|
||||
@@ -151,6 +163,8 @@ export class InterfaceService {
|
||||
info,
|
||||
gateway,
|
||||
showSsl: !!url,
|
||||
masked,
|
||||
ui,
|
||||
})
|
||||
}
|
||||
return res
|
||||
@@ -328,7 +342,7 @@ export class InterfaceService {
|
||||
}
|
||||
|
||||
private toDisplayAddress(
|
||||
{ info, url, gateway, showSsl }: AddressWithInfo,
|
||||
{ info, url, gateway, showSsl, masked, ui }: AddressWithInfo,
|
||||
publicDomains: Record<string, T.PublicDomainConfig>,
|
||||
): DisplayAddress {
|
||||
let access: DisplayAddress['access']
|
||||
@@ -509,6 +523,8 @@ export class InterfaceService {
|
||||
gatewayName,
|
||||
type,
|
||||
bullets,
|
||||
masked,
|
||||
ui,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -522,6 +538,7 @@ export type MappedServiceInterface = T.ServiceInterface & {
|
||||
common: DisplayAddress[]
|
||||
uncommon: DisplayAddress[]
|
||||
}
|
||||
addSsl: boolean
|
||||
}
|
||||
|
||||
export type InterfaceGateway = GatewayPlus & {
|
||||
@@ -534,4 +551,6 @@ export type DisplayAddress = {
|
||||
gatewayName: string | null
|
||||
url: string
|
||||
bullets: i18nKey[]
|
||||
masked: boolean
|
||||
ui: boolean
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ import { InterfaceComponent } from './interface.component'
|
||||
iconStart="@tui.trash"
|
||||
appearance="action-destructive"
|
||||
(click)="remove(domain)"
|
||||
[disabled]="!privateDomains()"
|
||||
>
|
||||
{{ 'Delete' | i18n }}
|
||||
</button>
|
||||
|
||||
@@ -31,7 +31,8 @@ import { PublicDomain, PublicDomainService } from './pd.service'
|
||||
tuiButton
|
||||
iconStart="@tui.plus"
|
||||
[style.margin-inline-start]="'auto'"
|
||||
(click)="service.add()"
|
||||
(click)="service.add(addSsl())"
|
||||
[disabled]="!publicDomains()"
|
||||
>
|
||||
{{ 'Add' | i18n }}
|
||||
</button>
|
||||
@@ -44,7 +45,7 @@ import { PublicDomain, PublicDomainService } from './pd.service'
|
||||
} @else {
|
||||
<table [appTable]="['Domain', 'Gateway', 'Certificate Authority', null]">
|
||||
@for (domain of publicDomains(); track $index) {
|
||||
<tr [publicDomain]="domain"></tr>
|
||||
<tr [publicDomain]="domain" [addSsl]="addSsl()"></tr>
|
||||
} @empty {
|
||||
@for (_ of [0]; track $index) {
|
||||
<tr>
|
||||
@@ -79,4 +80,6 @@ export class PublicDomainsComponent {
|
||||
readonly service = inject(PublicDomainService)
|
||||
|
||||
readonly publicDomains = input.required<readonly PublicDomain[] | undefined>()
|
||||
|
||||
readonly addSsl = input.required<boolean>()
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ import { toAuthorityName } from 'src/app/utils/acme'
|
||||
tuiOption
|
||||
new
|
||||
iconStart="@tui.pencil"
|
||||
(click)="service.edit(publicDomain())"
|
||||
(click)="service.edit(publicDomain(), addSsl())"
|
||||
>
|
||||
{{ 'Edit' | i18n }}
|
||||
</button>
|
||||
@@ -107,8 +107,11 @@ export class PublicDomainsItemComponent {
|
||||
open = false
|
||||
|
||||
readonly publicDomain = input.required<PublicDomain>()
|
||||
readonly addSsl = input.required<boolean>()
|
||||
|
||||
readonly authority = computed(() => toAuthorityName(this.publicDomain().acme))
|
||||
readonly authority = computed(() =>
|
||||
toAuthorityName(this.publicDomain().acme, this.addSsl()),
|
||||
)
|
||||
readonly dnsMessage = computed<i18nKey>(
|
||||
() =>
|
||||
`Create one of the DNS records below to cause ${this.publicDomain().fqdn} to resolve to ${this.publicDomain().gateway?.ipInfo.wanIp}` as i18nKey,
|
||||
|
||||
@@ -58,7 +58,7 @@ export class PublicDomainService {
|
||||
),
|
||||
)
|
||||
|
||||
async add() {
|
||||
async add(addSsl: boolean) {
|
||||
const addSpec = ISB.InputSpec.of({
|
||||
fqdn: ISB.Value.text({
|
||||
name: this.i18n.transform('Domain'),
|
||||
@@ -69,7 +69,10 @@ export class PublicDomainService {
|
||||
default: null,
|
||||
patterns: [utils.Patterns.domain],
|
||||
}).map(f => f.toLocaleLowerCase()),
|
||||
...this.gatewayAndAuthoritySpec(),
|
||||
...this.gatewaySpec(),
|
||||
...(addSsl
|
||||
? this.authoritySpec()
|
||||
: ({} as ReturnType<typeof this.authoritySpec>)),
|
||||
})
|
||||
|
||||
this.formDialog.open(FormComponent, {
|
||||
@@ -87,9 +90,12 @@ export class PublicDomainService {
|
||||
})
|
||||
}
|
||||
|
||||
async edit(domain: PublicDomain) {
|
||||
async edit(domain: PublicDomain, addSsl: boolean) {
|
||||
const editSpec = ISB.InputSpec.of({
|
||||
...this.gatewayAndAuthoritySpec(),
|
||||
...this.gatewaySpec(),
|
||||
...(addSsl
|
||||
? this.authoritySpec()
|
||||
: ({} as ReturnType<typeof this.authoritySpec>)),
|
||||
})
|
||||
|
||||
this.formDialog.open(FormComponent, {
|
||||
@@ -155,7 +161,7 @@ export class PublicDomainService {
|
||||
private async save(
|
||||
fqdn: string,
|
||||
gatewayId: string,
|
||||
authority: 'local' | string,
|
||||
authority?: 'local' | string,
|
||||
) {
|
||||
const gateway = this.data()!.gateways.find(g => g.id === gatewayId)!
|
||||
|
||||
@@ -163,7 +169,7 @@ export class PublicDomainService {
|
||||
const params = {
|
||||
fqdn,
|
||||
gateway: gatewayId,
|
||||
acme: authority === 'local' ? null : authority,
|
||||
acme: !authority || authority === 'local' ? null : authority,
|
||||
}
|
||||
try {
|
||||
let ip: string | null
|
||||
@@ -225,7 +231,7 @@ export class PublicDomainService {
|
||||
}
|
||||
}
|
||||
|
||||
private gatewayAndAuthoritySpec() {
|
||||
private gatewaySpec() {
|
||||
const data = this.data()!
|
||||
|
||||
const gateways = data.gateways.filter(
|
||||
@@ -251,6 +257,13 @@ export class PublicDomainService {
|
||||
.filter(g => !g.ipInfo.wanIp || utils.CGNAT.contains(g.ipInfo.wanIp))
|
||||
.map(g => g.id),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
private authoritySpec() {
|
||||
const data = this.data()!
|
||||
|
||||
return {
|
||||
authority: ISB.Value.select({
|
||||
name: this.i18n.transform('Certificate Authority'),
|
||||
description: this.i18n.transform(
|
||||
|
||||
@@ -50,6 +50,7 @@ type OnionForm = {
|
||||
iconStart="@tui.plus"
|
||||
[style.margin-inline-start]="'auto'"
|
||||
(click)="add()"
|
||||
[disabled]="!torDomains()"
|
||||
>
|
||||
{{ 'Add' | i18n }}
|
||||
</button>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Component, ElementRef, Input, ViewChild } from '@angular/core'
|
||||
import {
|
||||
INTERSECTION_ROOT,
|
||||
WA_INTERSECTION_ROOT,
|
||||
WaIntersectionObserver,
|
||||
} from '@ng-web-apis/intersection-observer'
|
||||
import { WaMutationObserver } from '@ng-web-apis/mutation-observer'
|
||||
@@ -31,12 +31,7 @@ import { LogsPipe } from './logs.pipe'
|
||||
LogsPipe,
|
||||
i18nPipe,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: INTERSECTION_ROOT,
|
||||
useExisting: ElementRef,
|
||||
},
|
||||
],
|
||||
providers: [{ provide: WA_INTERSECTION_ROOT, useExisting: ElementRef }],
|
||||
})
|
||||
export class LogsComponent {
|
||||
@ViewChild('bottom')
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
signal,
|
||||
viewChild,
|
||||
} from '@angular/core'
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import {
|
||||
ErrorService,
|
||||
@@ -14,12 +15,15 @@ import {
|
||||
LoadingService,
|
||||
} from '@start9labs/shared'
|
||||
import { TuiButton } from '@taiga-ui/core'
|
||||
import { filter } from 'rxjs'
|
||||
import { distinctUntilChanged, skip } from 'rxjs/operators'
|
||||
import {
|
||||
RR,
|
||||
ServerNotification,
|
||||
ServerNotifications,
|
||||
} from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { BadgeService } from 'src/app/services/badge.service'
|
||||
import { NotificationService } from 'src/app/services/notification.service'
|
||||
import { TitleDirective } from 'src/app/services/title.service'
|
||||
import { NotificationsTableComponent } from './table.component'
|
||||
@@ -36,13 +40,13 @@ import { NotificationsTableComponent } from './table.component'
|
||||
iconStart="@tui.trash"
|
||||
appearance="primary-destructive"
|
||||
[style.margin]="'0 0.5rem 0 auto'"
|
||||
[disabled]="!tableNotifications()?.selected()?.length"
|
||||
[disabled]="!table()?.selected()?.length"
|
||||
(click)="remove(notifications() || [])"
|
||||
>
|
||||
{{ 'Delete selected' | i18n }}
|
||||
</button>
|
||||
</header>
|
||||
<div #table [notifications]="notifications()"></div>
|
||||
<div [notifications]="notifications()"></div>
|
||||
</section>
|
||||
`,
|
||||
styles: `
|
||||
@@ -64,20 +68,26 @@ export default class NotificationsComponent implements OnInit {
|
||||
readonly errorService = inject(ErrorService)
|
||||
readonly notifications = signal<ServerNotifications | null>(null)
|
||||
|
||||
protected tableNotifications =
|
||||
viewChild<NotificationsTableComponent<ServerNotification<number>>>('table')
|
||||
protected readonly table = viewChild<
|
||||
NotificationsTableComponent<ServerNotification<number>>
|
||||
>(NotificationsTableComponent)
|
||||
|
||||
protected readonly badge = inject(BadgeService)
|
||||
.getCount('notifications')
|
||||
.pipe(
|
||||
distinctUntilChanged(),
|
||||
filter(Boolean),
|
||||
skip(1),
|
||||
takeUntilDestroyed(),
|
||||
)
|
||||
.subscribe(() => this.init())
|
||||
|
||||
ngOnInit() {
|
||||
this.route.queryParams.subscribe(params => {
|
||||
this.router.navigate([], { relativeTo: this.route, queryParams: {} })
|
||||
|
||||
if (isEmptyObject(params)) {
|
||||
this.getMore({}).then(() => {
|
||||
const latest = this.notifications()?.at(0)
|
||||
if (latest) {
|
||||
this.service.markSeenAll(latest.id)
|
||||
}
|
||||
})
|
||||
this.init()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -93,7 +103,7 @@ export default class NotificationsComponent implements OnInit {
|
||||
|
||||
async remove(all: ServerNotifications) {
|
||||
const ids =
|
||||
this.tableNotifications()
|
||||
this.table()
|
||||
?.selected()
|
||||
.map(n => n.id) || []
|
||||
const loader = this.loader.open('Deleting').subscribe()
|
||||
@@ -107,4 +117,13 @@ export default class NotificationsComponent implements OnInit {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private init() {
|
||||
this.getMore({}).then(() => {
|
||||
const latest = this.notifications()?.at(0)
|
||||
if (latest) {
|
||||
this.service.markSeenAll(latest.id)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
Input,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
@@ -11,15 +12,15 @@ import { TuiIcon, TuiLoader } from '@taiga-ui/core'
|
||||
@Component({
|
||||
selector: 'tr[healthCheck]',
|
||||
template: `
|
||||
<td>{{ healthCheck.name }}</td>
|
||||
<td class="name">{{ healthCheck().name }}</td>
|
||||
<td>
|
||||
<span>
|
||||
@if (loading) {
|
||||
@if (loading()) {
|
||||
<tui-loader size="m" />
|
||||
} @else {
|
||||
<tui-icon [icon]="icon" [style.color]="color" />
|
||||
<tui-icon [icon]="icon()" [style.color]="color()" />
|
||||
}
|
||||
{{ message }}
|
||||
{{ message() }}
|
||||
</span>
|
||||
</td>
|
||||
`,
|
||||
@@ -30,17 +31,25 @@ import { TuiIcon, TuiLoader } from '@taiga-ui/core'
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.name {
|
||||
width: 9.5rem;
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
td:first-child {
|
||||
font-weight: bold;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
td {
|
||||
width: 100%;
|
||||
|
||||
td:last-child {
|
||||
color: var(--tui-text-secondary);
|
||||
&:first-child {
|
||||
font-weight: bold;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
color: var(--tui-text-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
@@ -48,19 +57,17 @@ import { TuiIcon, TuiLoader } from '@taiga-ui/core'
|
||||
imports: [TuiLoader, TuiIcon],
|
||||
})
|
||||
export class ServiceHealthCheckComponent {
|
||||
@Input({ required: true })
|
||||
healthCheck!: T.NamedHealthCheckResult
|
||||
|
||||
private readonly i18n = inject(i18nPipe)
|
||||
|
||||
get loading(): boolean {
|
||||
const { result } = this.healthCheck
|
||||
readonly healthCheck = input.required<T.NamedHealthCheckResult>()
|
||||
|
||||
return !result || result === 'starting' || result === 'loading'
|
||||
}
|
||||
readonly loading = computed(
|
||||
({ result } = this.healthCheck()) =>
|
||||
!result || ['starting', 'loading', 'waiting'].includes(result),
|
||||
)
|
||||
|
||||
get icon(): string {
|
||||
switch (this.healthCheck.result) {
|
||||
readonly icon = computed(() => {
|
||||
switch (this.healthCheck().result) {
|
||||
case 'success':
|
||||
return '@tui.check'
|
||||
case 'failure':
|
||||
@@ -68,10 +75,10 @@ export class ServiceHealthCheckComponent {
|
||||
default:
|
||||
return '@tui.minus'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
get color(): string {
|
||||
switch (this.healthCheck.result) {
|
||||
readonly color = computed(() => {
|
||||
switch (this.healthCheck().result) {
|
||||
case 'success':
|
||||
return 'var(--tui-status-positive)'
|
||||
case 'failure':
|
||||
@@ -79,26 +86,30 @@ export class ServiceHealthCheckComponent {
|
||||
case 'starting':
|
||||
case 'loading':
|
||||
return 'var(--tui-background-accent-1)'
|
||||
// disabled
|
||||
// disabled and waiting
|
||||
default:
|
||||
return 'var(--tui-text-secondary)'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
get message(): string {
|
||||
if (!this.healthCheck.result) {
|
||||
readonly message = computed(({ result, message } = this.healthCheck()) => {
|
||||
if (!result) {
|
||||
return this.i18n.transform('Awaiting result')!
|
||||
}
|
||||
|
||||
switch (this.healthCheck.result) {
|
||||
switch (result) {
|
||||
case 'starting':
|
||||
return this.i18n.transform('Starting')!
|
||||
case 'success':
|
||||
return `${this.i18n.transform('Success')}: ${this.healthCheck.message || 'health check passing'}`
|
||||
return `${this.i18n.transform('Success')}: ${message || 'health check passing'}`
|
||||
case 'waiting':
|
||||
return message
|
||||
? `${this.i18n.transform('Waiting on')} ${message}...`
|
||||
: `${this.i18n.transform('Waiting')}...`
|
||||
case 'loading':
|
||||
case 'failure':
|
||||
case 'disabled':
|
||||
return this.healthCheck.message || this.healthCheck.result
|
||||
return message || result
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
Input,
|
||||
OnChanges,
|
||||
computed,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import { tuiPure } from '@taiga-ui/cdk'
|
||||
import { ServiceUptimeComponent } from 'src/app/routes/portal/routes/services/components/uptime.component'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
import { PkgDependencyErrors } from 'src/app/services/dep-error.service'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
@@ -19,17 +16,17 @@ import { StatusComponent } from './status.component'
|
||||
selector: 'tr[appService]',
|
||||
template: `
|
||||
<td [style.width.rem]="3" [style.grid-area]="'1 / 1 / 4'">
|
||||
<img alt="logo" [src]="pkg.icon" />
|
||||
<img alt="logo" [src]="pkg().icon" />
|
||||
</td>
|
||||
<td class="title">
|
||||
<a [routerLink]="routerLink">{{ manifest.title }}</a>
|
||||
<a [routerLink]="'/services/' + manifest().id">{{ manifest().title }}</a>
|
||||
</td>
|
||||
<td class="status" [style.grid-area]="'3 / 2'">
|
||||
<app-status [pkg]="pkg" [hasDepErrors]="hasError(depErrors)" />
|
||||
<app-status [pkg]="pkg()" [hasDepErrors]="hasError()" />
|
||||
</td>
|
||||
<td class="version">{{ manifest.version }}</td>
|
||||
<td class="version">{{ manifest().version }}</td>
|
||||
<td class="uptime">
|
||||
@if (pkg.statusInfo.started; as started) {
|
||||
@if (pkg().statusInfo.started; as started) {
|
||||
<span>{{ 'Uptime' | i18n }}:</span>
|
||||
<service-uptime [started]="started" />
|
||||
} @else {
|
||||
@@ -143,39 +140,15 @@ import { StatusComponent } from './status.component'
|
||||
}
|
||||
}
|
||||
`,
|
||||
hostDirectives: [RouterLink],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [RouterLink, StatusComponent, ServiceUptimeComponent, i18nPipe],
|
||||
})
|
||||
export class ServiceComponent implements OnChanges {
|
||||
private readonly link = inject(RouterLink)
|
||||
export class ServiceComponent {
|
||||
readonly pkg = input.required<PackageDataEntry>()
|
||||
readonly depErrors = input<PkgDependencyErrors>({})
|
||||
|
||||
@Input()
|
||||
pkg!: PackageDataEntry
|
||||
|
||||
@Input()
|
||||
depErrors?: PkgDependencyErrors
|
||||
|
||||
readonly connected$ = inject(ConnectionService)
|
||||
|
||||
get installed(): boolean {
|
||||
return this.pkg.stateInfo.state === 'installed'
|
||||
}
|
||||
|
||||
get manifest() {
|
||||
return getManifest(this.pkg)
|
||||
}
|
||||
|
||||
get routerLink() {
|
||||
return `/services/${this.manifest.id}`
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
this.link.routerLink = this.routerLink
|
||||
}
|
||||
|
||||
@tuiPure
|
||||
hasError(errors: PkgDependencyErrors = {}): boolean {
|
||||
return Object.values(errors).some(Boolean)
|
||||
}
|
||||
readonly manifest = computed(() => getManifest(this.pkg()))
|
||||
readonly hasError = computed(() =>
|
||||
Object.values(this.depErrors()).some(Boolean),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -60,8 +60,9 @@ import { RouterLink } from '@angular/router'
|
||||
@for (service of services() | tuiTableSort; track $index) {
|
||||
<tr
|
||||
appService
|
||||
[routerLink]="'/services/' + (service | toManifest)?.id"
|
||||
[pkg]="service"
|
||||
[depErrors]="errors()?.[(service | toManifest).id]"
|
||||
[depErrors]="errors()?.[(service | toManifest).id] || {}"
|
||||
></tr>
|
||||
} @empty {
|
||||
@for (_ of ['', '']; track $index) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AsyncPipe } from '@angular/common'
|
||||
import { Component, inject } from '@angular/core'
|
||||
import { Component, inject, signal } from '@angular/core'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import {
|
||||
DialogService,
|
||||
getErrorMessage,
|
||||
@@ -17,7 +17,7 @@ import { injectContext } from '@taiga-ui/polymorpheus'
|
||||
import * as json from 'fast-json-patch'
|
||||
import { compare } from 'fast-json-patch'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { catchError, defer, EMPTY, endWith, firstValueFrom, map } from 'rxjs'
|
||||
import { catchError, EMPTY, endWith, firstValueFrom, from, map } from 'rxjs'
|
||||
import {
|
||||
ActionButton,
|
||||
FormComponent,
|
||||
@@ -50,13 +50,12 @@ export type PackageActionData = {
|
||||
<img [src]="pkgInfo.icon" alt="" />
|
||||
<h4>{{ pkgInfo.title }}</h4>
|
||||
</div>
|
||||
@if (res$ | async; as res) {
|
||||
@if (error) {
|
||||
<tui-notification appearance="negative">
|
||||
<div [innerHTML]="error"></div>
|
||||
</tui-notification>
|
||||
}
|
||||
|
||||
@if (error()) {
|
||||
<tui-notification appearance="negative">
|
||||
<div [innerHTML]="error()"></div>
|
||||
</tui-notification>
|
||||
}
|
||||
@if (res(); as res) {
|
||||
@if (warning) {
|
||||
<tui-notification appearance="warning">
|
||||
<div [innerHTML]="warning"></div>
|
||||
@@ -85,7 +84,7 @@ export type PackageActionData = {
|
||||
{{ 'Reset defaults' | i18n }}
|
||||
</button>
|
||||
</app-form>
|
||||
} @else {
|
||||
} @else if (!error()) {
|
||||
<tui-loader size="l" textContent="loading" />
|
||||
}
|
||||
`,
|
||||
@@ -111,7 +110,6 @@ export type PackageActionData = {
|
||||
}
|
||||
`,
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
TuiNotification,
|
||||
TuiLoader,
|
||||
TuiButton,
|
||||
@@ -143,38 +141,39 @@ export class ActionInputModal {
|
||||
},
|
||||
]
|
||||
|
||||
error = ''
|
||||
readonly error = signal('')
|
||||
readonly res = toSignal(
|
||||
from(
|
||||
this.api.getActionInput({
|
||||
packageId: this.pkgInfo.id,
|
||||
actionId: this.actionId,
|
||||
}),
|
||||
).pipe(
|
||||
map(res => {
|
||||
console.warn('MAP', res)
|
||||
const originalValue = res.value || {}
|
||||
this.eventId = res.eventId
|
||||
|
||||
res$ = defer(() =>
|
||||
this.api.getActionInput({
|
||||
packageId: this.pkgInfo.id,
|
||||
actionId: this.actionId,
|
||||
}),
|
||||
).pipe(
|
||||
map(res => {
|
||||
console.warn('MAP', res)
|
||||
const originalValue = res.value || {}
|
||||
this.eventId = res.eventId
|
||||
|
||||
return {
|
||||
spec: res.spec,
|
||||
originalValue,
|
||||
operations: this.requestInfo?.input
|
||||
? compare(
|
||||
JSON.parse(JSON.stringify(originalValue)),
|
||||
utils.deepMerge(
|
||||
return {
|
||||
spec: res.spec,
|
||||
originalValue,
|
||||
operations: this.requestInfo?.input
|
||||
? compare(
|
||||
JSON.parse(JSON.stringify(originalValue)),
|
||||
this.requestInfo.input.value,
|
||||
) as object,
|
||||
)
|
||||
: null,
|
||||
}
|
||||
}),
|
||||
catchError(e => {
|
||||
console.error('catchError', e)
|
||||
this.error = String(getErrorMessage(e))
|
||||
return EMPTY
|
||||
}),
|
||||
utils.deepMerge(
|
||||
JSON.parse(JSON.stringify(originalValue)),
|
||||
this.requestInfo.input.value,
|
||||
) as object,
|
||||
)
|
||||
: null,
|
||||
}
|
||||
}),
|
||||
catchError(e => {
|
||||
console.error('catchError', e)
|
||||
this.error.set(String(getErrorMessage(e)))
|
||||
return EMPTY
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
async execute(input: object) {
|
||||
|
||||
@@ -2,13 +2,12 @@ import { CommonModule } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ElementRef,
|
||||
inject,
|
||||
Input,
|
||||
ViewChild,
|
||||
} from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import { TuiButton, TuiTextfield, TuiTextfieldDirective } from '@taiga-ui/core'
|
||||
import { CopyService, i18nPipe } from '@start9labs/shared'
|
||||
import { TuiButton, TuiTextfield } from '@taiga-ui/core'
|
||||
import { QrCodeComponent } from 'ng-qrcode'
|
||||
import { SingleResult } from './types'
|
||||
|
||||
@@ -48,7 +47,7 @@ import { SingleResult } from './types'
|
||||
tabindex="-1"
|
||||
iconStart="@tui.copy"
|
||||
[style.pointer-events]="'auto'"
|
||||
(click)="copy()"
|
||||
(click)="copy.copy(single.value)"
|
||||
>
|
||||
{{ 'Copy' | i18n }}
|
||||
</button>
|
||||
@@ -96,25 +95,10 @@ import { SingleResult } from './types'
|
||||
],
|
||||
})
|
||||
export class ActionSuccessSingleComponent {
|
||||
@ViewChild(TuiTextfieldDirective, { read: ElementRef })
|
||||
private readonly input!: ElementRef<HTMLInputElement>
|
||||
readonly copy = inject(CopyService)
|
||||
|
||||
@Input()
|
||||
single!: SingleResult
|
||||
|
||||
masked = true
|
||||
|
||||
copy() {
|
||||
const el = this.input.nativeElement
|
||||
|
||||
if (!el) {
|
||||
return
|
||||
}
|
||||
|
||||
el.type = 'text'
|
||||
el.focus()
|
||||
el.select()
|
||||
el.ownerDocument.execCommand('copy')
|
||||
el.type = this.masked && this.single.masked ? 'password' : 'text'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,11 @@ export default class ServiceAboutRoute {
|
||||
private readonly markdown = inject(DialogService).openComponent(MARKDOWN, {
|
||||
label: 'License',
|
||||
size: 'l',
|
||||
data: from(inject(ApiService).getStaticInstalled(this.pkgId, 'LICENSE.md')),
|
||||
data: from(
|
||||
inject(ApiService).getStatic(
|
||||
`/s9pk/installed/${this.pkgId}.s9pk/LICENSE.md`,
|
||||
),
|
||||
),
|
||||
})
|
||||
|
||||
readonly groups = toSignal<{ header: i18nKey; items: AdditionalItem[] }[]>(
|
||||
|
||||
@@ -142,6 +142,7 @@ export default class ServiceInterfaceRoute {
|
||||
torDomains: host.onions,
|
||||
publicDomains: getPublicDomains(host.publicDomains, gateways),
|
||||
privateDomains: host.privateDomains,
|
||||
addSsl: !!binding?.options.addSsl,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -69,7 +69,6 @@ import { BACKUP_RESTORE } from './restore.component'
|
||||
path="/user-manual/backup-create.html"
|
||||
appearance="action-grayscale"
|
||||
iconEnd="@tui.external-link"
|
||||
[pseudo]="true"
|
||||
[textContent]="'View instructions' | i18n"
|
||||
></a>
|
||||
} @else {
|
||||
@@ -83,7 +82,6 @@ import { BACKUP_RESTORE } from './restore.component'
|
||||
path="/user-manual/backup-restore.html"
|
||||
appearance="action-grayscale"
|
||||
iconEnd="@tui.external-link"
|
||||
[pseudo]="true"
|
||||
[textContent]="'View instructions' | i18n"
|
||||
></a>
|
||||
}
|
||||
@@ -127,7 +125,6 @@ import { BACKUP_RESTORE } from './restore.component'
|
||||
fragment="#network-folder"
|
||||
appearance="action-grayscale"
|
||||
iconEnd="@tui.external-link"
|
||||
[pseudo]="true"
|
||||
[textContent]="'View instructions' | i18n"
|
||||
></a>
|
||||
</section>
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
LoadingService,
|
||||
} from '@start9labs/shared'
|
||||
import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile'
|
||||
import { TuiContext, TuiStringHandler } from '@taiga-ui/cdk'
|
||||
import { TuiAnimated, TuiContext, TuiStringHandler } from '@taiga-ui/cdk'
|
||||
import {
|
||||
TuiAppearance,
|
||||
TuiButton,
|
||||
@@ -168,7 +168,7 @@ import { SystemWipeComponent } from './wipe.component'
|
||||
</button>
|
||||
</div>
|
||||
@if (count > 4) {
|
||||
<div tuiCell tuiAppearance="outline-grayscale" @tuiScaleIn @tuiFadeIn>
|
||||
<div tuiCell tuiAppearance="outline-grayscale" tuiAnimated>
|
||||
<tui-icon icon="@tui.briefcase-medical" />
|
||||
<span tuiTitle>
|
||||
<strong>{{ 'Disk Repair' | i18n }}</strong>
|
||||
@@ -209,10 +209,14 @@ import { SystemWipeComponent } from './wipe.component'
|
||||
[tuiCell] {
|
||||
background: var(--tui-background-neutral-1);
|
||||
}
|
||||
|
||||
[tuiAnimated].tui-enter,
|
||||
[tuiAnimated].tui-leave {
|
||||
animation-name: tuiFade, tuiScale;
|
||||
}
|
||||
`,
|
||||
providers: [tuiCellOptionsProvider({ height: 'spacious' })],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations: [tuiScaleIn, tuiFadeIn],
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
RouterLink,
|
||||
@@ -231,6 +235,7 @@ import { SystemWipeComponent } from './wipe.component'
|
||||
SnekDirective,
|
||||
TuiBadge,
|
||||
TuiBadgeNotification,
|
||||
TuiAnimated,
|
||||
],
|
||||
})
|
||||
export default class SystemGeneralComponent {
|
||||
|
||||
@@ -103,6 +103,7 @@ export default class StartOsUiComponent {
|
||||
torDomains: network.host.onions,
|
||||
publicDomains: getPublicDomains(network.host.publicDomains, gateways),
|
||||
privateDomains: network.host.privateDomains,
|
||||
addSsl: true,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -43,11 +43,11 @@ export const SYSTEM_MENU = [
|
||||
item: 'Gateways',
|
||||
link: 'gateways',
|
||||
},
|
||||
{
|
||||
icon: '@tui.award',
|
||||
item: 'Certificate Authorities',
|
||||
link: 'authorities',
|
||||
},
|
||||
// {
|
||||
// icon: '@tui.award',
|
||||
// item: 'Certificate Authorities',
|
||||
// link: 'authorities',
|
||||
// },
|
||||
{
|
||||
icon: '@tui.globe',
|
||||
item: 'DNS' as i18nKey,
|
||||
|
||||
@@ -71,12 +71,12 @@ export default [
|
||||
path: 'gateways',
|
||||
loadComponent: () => import('./routes/gateways/gateways.component'),
|
||||
},
|
||||
{
|
||||
path: 'authorities',
|
||||
title: titleResolver,
|
||||
loadComponent: () =>
|
||||
import('./routes/authorities/authorities.component'),
|
||||
},
|
||||
// {
|
||||
// path: 'authorities',
|
||||
// title: titleResolver,
|
||||
// loadComponent: () =>
|
||||
// import('./routes/authorities/authorities.component'),
|
||||
// },
|
||||
{
|
||||
path: 'dns',
|
||||
title: titleResolver,
|
||||
|
||||
@@ -385,7 +385,7 @@ export namespace Mock {
|
||||
docsUrl: 'https://bitcoin.org',
|
||||
releaseNotes: 'Even better support for Bitcoin and wallets!',
|
||||
osVersion: '0.3.6',
|
||||
sdkVersion: '0.4.0-beta.45',
|
||||
sdkVersion: '0.4.0-beta.46',
|
||||
gitHash: 'fakehash',
|
||||
icon: BTC_ICON,
|
||||
sourceVersion: null,
|
||||
@@ -420,7 +420,7 @@ export namespace Mock {
|
||||
docsUrl: 'https://bitcoinknots.org',
|
||||
releaseNotes: 'Even better support for Bitcoin and wallets!',
|
||||
osVersion: '0.3.6',
|
||||
sdkVersion: '0.4.0-beta.45',
|
||||
sdkVersion: '0.4.0-beta.46',
|
||||
gitHash: 'fakehash',
|
||||
icon: BTC_ICON,
|
||||
sourceVersion: null,
|
||||
@@ -465,7 +465,7 @@ export namespace Mock {
|
||||
docsUrl: 'https://bitcoin.org',
|
||||
releaseNotes: 'Even better support for Bitcoin and wallets!',
|
||||
osVersion: '0.3.6',
|
||||
sdkVersion: '0.4.0-beta.45',
|
||||
sdkVersion: '0.4.0-beta.46',
|
||||
gitHash: 'fakehash',
|
||||
icon: BTC_ICON,
|
||||
sourceVersion: null,
|
||||
@@ -500,7 +500,7 @@ export namespace Mock {
|
||||
docsUrl: 'https://bitcoinknots.org',
|
||||
releaseNotes: 'Even better support for Bitcoin and wallets!',
|
||||
osVersion: '0.3.6',
|
||||
sdkVersion: '0.4.0-beta.45',
|
||||
sdkVersion: '0.4.0-beta.46',
|
||||
gitHash: 'fakehash',
|
||||
icon: BTC_ICON,
|
||||
sourceVersion: null,
|
||||
@@ -547,7 +547,7 @@ export namespace Mock {
|
||||
docsUrl: 'https://lightning.engineering/',
|
||||
releaseNotes: 'Upstream release to 0.17.5',
|
||||
osVersion: '0.3.6',
|
||||
sdkVersion: '0.4.0-beta.45',
|
||||
sdkVersion: '0.4.0-beta.46',
|
||||
gitHash: 'fakehash',
|
||||
icon: LND_ICON,
|
||||
sourceVersion: null,
|
||||
@@ -595,7 +595,7 @@ export namespace Mock {
|
||||
docsUrl: 'https://lightning.engineering/',
|
||||
releaseNotes: 'Upstream release to 0.17.4',
|
||||
osVersion: '0.3.6',
|
||||
sdkVersion: '0.4.0-beta.45',
|
||||
sdkVersion: '0.4.0-beta.46',
|
||||
gitHash: 'fakehash',
|
||||
icon: LND_ICON,
|
||||
sourceVersion: null,
|
||||
@@ -647,7 +647,7 @@ export namespace Mock {
|
||||
docsUrl: 'https://bitcoin.org',
|
||||
releaseNotes: 'Even better support for Bitcoin and wallets!',
|
||||
osVersion: '0.3.6',
|
||||
sdkVersion: '0.4.0-beta.45',
|
||||
sdkVersion: '0.4.0-beta.46',
|
||||
gitHash: 'fakehash',
|
||||
icon: BTC_ICON,
|
||||
sourceVersion: null,
|
||||
@@ -682,7 +682,7 @@ export namespace Mock {
|
||||
docsUrl: 'https://bitcoinknots.org',
|
||||
releaseNotes: 'Even better support for Bitcoin and wallets!',
|
||||
osVersion: '0.3.6',
|
||||
sdkVersion: '0.4.0-beta.45',
|
||||
sdkVersion: '0.4.0-beta.46',
|
||||
gitHash: 'fakehash',
|
||||
icon: BTC_ICON,
|
||||
sourceVersion: null,
|
||||
@@ -727,7 +727,7 @@ export namespace Mock {
|
||||
docsUrl: 'https://lightning.engineering/',
|
||||
releaseNotes: 'Upstream release and minor fixes.',
|
||||
osVersion: '0.3.6',
|
||||
sdkVersion: '0.4.0-beta.45',
|
||||
sdkVersion: '0.4.0-beta.46',
|
||||
gitHash: 'fakehash',
|
||||
icon: LND_ICON,
|
||||
sourceVersion: null,
|
||||
@@ -775,7 +775,7 @@ export namespace Mock {
|
||||
marketingSite: '',
|
||||
releaseNotes: 'Upstream release and minor fixes.',
|
||||
osVersion: '0.3.6',
|
||||
sdkVersion: '0.4.0-beta.45',
|
||||
sdkVersion: '0.4.0-beta.46',
|
||||
gitHash: 'fakehash',
|
||||
icon: PROXY_ICON,
|
||||
sourceVersion: null,
|
||||
|
||||
@@ -15,10 +15,7 @@ export abstract class ApiService {
|
||||
path: 'LICENSE.md',
|
||||
): Promise<string>
|
||||
|
||||
abstract getStaticInstalled(
|
||||
id: T.PackageId,
|
||||
path: 'LICENSE.md',
|
||||
): Promise<string>
|
||||
abstract getStatic(url: string): Promise<string>
|
||||
|
||||
// websocket
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
HttpOptions,
|
||||
HttpService,
|
||||
isRpcError,
|
||||
Method,
|
||||
RpcError,
|
||||
RPCOptions,
|
||||
} from '@start9labs/shared'
|
||||
@@ -37,7 +36,7 @@ export class LiveApiService extends ApiService {
|
||||
|
||||
async uploadFile(guid: string, body: Blob): Promise<void> {
|
||||
await this.httpRequest({
|
||||
method: Method.POST,
|
||||
method: 'POST',
|
||||
body,
|
||||
url: `/rest/rpc/${guid}`,
|
||||
timeout: 0,
|
||||
@@ -53,7 +52,7 @@ export class LiveApiService extends ApiService {
|
||||
const encodedUrl = encodeURIComponent(pkg.s9pk.url)
|
||||
|
||||
return this.httpRequest({
|
||||
method: Method.GET,
|
||||
method: 'GET',
|
||||
url: `/s9pk/proxy/${encodedUrl}/${path}`,
|
||||
params: {
|
||||
rootSighash: pkg.s9pk.commitment.rootSighash,
|
||||
@@ -63,13 +62,10 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async getStaticInstalled(
|
||||
id: T.PackageId,
|
||||
path: 'LICENSE.md',
|
||||
): Promise<string> {
|
||||
async getStatic(url: string): Promise<string> {
|
||||
return this.httpRequest({
|
||||
method: Method.GET,
|
||||
url: `/s9pk/installed/${id}.s9pk/${path}`,
|
||||
method: 'GET',
|
||||
url,
|
||||
responseType: 'text',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -83,10 +83,7 @@ export class MockApiService extends ApiService {
|
||||
return markdown
|
||||
}
|
||||
|
||||
async getStaticInstalled(
|
||||
id: T.PackageId,
|
||||
path: 'LICENSE.md',
|
||||
): Promise<string> {
|
||||
async getStatic(url: string): Promise<string> {
|
||||
await pauseFor(2000)
|
||||
return markdown
|
||||
}
|
||||
@@ -1125,8 +1122,8 @@ export class MockApiService extends ApiService {
|
||||
},
|
||||
'p2p-interface': {
|
||||
name: 'P2P Interface',
|
||||
result: 'success',
|
||||
message: null,
|
||||
result: 'waiting',
|
||||
message: 'Chain State',
|
||||
},
|
||||
'rpc-interface': {
|
||||
name: 'RPC Interface',
|
||||
|
||||
@@ -163,16 +163,3 @@ export const PrimaryRendering: Record<PrimaryStatus, StatusRendering> = {
|
||||
showDots: false,
|
||||
},
|
||||
}
|
||||
|
||||
export const DependencyRendering: Record<DependencyStatus, StatusRendering> = {
|
||||
warning: { display: 'Issue', color: 'warning' },
|
||||
satisfied: { display: 'Satisfied', color: 'success' },
|
||||
}
|
||||
|
||||
export const HealthRendering: Record<T.HealthStatus, StatusRendering> = {
|
||||
failure: { display: 'Failure', color: 'danger' },
|
||||
starting: { display: 'Starting', color: 'primary' },
|
||||
loading: { display: 'Loading', color: 'primary' },
|
||||
success: { display: 'Healthy', color: 'success' },
|
||||
disabled: { display: 'Disabled', color: 'dark' },
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
export function toAuthorityName(url: string | null): string | 'Local Root CA' {
|
||||
return (
|
||||
knownAuthorities.find(ca => ca.url === url)?.name || url || 'Local Root CA'
|
||||
)
|
||||
export function toAuthorityName(
|
||||
url: string | null,
|
||||
addSsl = true,
|
||||
): string | 'Local Root CA' | '-' {
|
||||
if (url) {
|
||||
return knownAuthorities.find(ca => ca.url === url)?.name || url
|
||||
} else {
|
||||
return addSsl ? 'Local Root CA' : '-'
|
||||
}
|
||||
}
|
||||
|
||||
export function toAuthorityUrl(name: string): string {
|
||||
|
||||
Reference in New Issue
Block a user